From 9cc03113bb5b544d411fa3ef5427af51aad6f93e Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Tue, 21 Apr 2026 14:24:37 +0000 Subject: [PATCH 01/23] chore: format --- CHANGELOG.md | 3 +-- packages/react-router-dev/CHANGELOG.md | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 085388ac58..e582ec4b88 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -224,10 +224,9 @@ Date: 2026-04-20 ### Unstable Changes -⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ +⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ - `@react-router/dev` - For `unstable_reactRouterRSC` Vite plugin consumers, require `@vitejs/plugin-react` in user Vite config, and more reliably split route modules. ([#14965](https://github.com/remix-run/react-router/pull/14965)) ([[aabf4a1](https://github.com/remix-run/react-router/commit/aabf4a1)) - - ⚠️ This is a breaking change if you have begun using the `unstable_reactRouterRSC` Vite plugin - please install `@vitejs/plugin-react` and add the `react` plugin to your Vite plugins array. **Full Changelog**: [`v7.14.1...v7.14.2`](https://github.com/remix-run/react-router/compare/react-router@7.14.1...react-router@7.14.2) diff --git a/packages/react-router-dev/CHANGELOG.md b/packages/react-router-dev/CHANGELOG.md index b0fbfbb3d8..4830dafa98 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -11,11 +11,11 @@ ### Unstable Changes -⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ +⚠️ _[Unstable features](https://reactrouter.com/community/api-development-strategy#unstable-flags) are not recommended for production use_ - For `unstable_reactRouterRSC` Vite plugin consumers, require `@vitejs/plugin-react` in user Vite config, and more reliably split route modules. ([#14965](https://github.com/remix-run/react-router/pull/14965)) ([[aabf4a1](https://github.com/remix-run/react-router/commit/aabf4a1)) - - ⚠️ This is a breaking change if you have begun using the `unstable_reactRouterRSC` Vite plugin - please install `@vitejs/plugin-react` and add the `react` plugin to your Vite plugins array. + - Updated dependencies: - [`react-router@7.14.2`](https://github.com/remix-run/react-router/releases/tag/react-router@7.14.2) - [`@react-router/node@7.14.2`](https://github.com/remix-run/react-router/releases/tag/@react-router/node@7.14.2) From 596655c8fda6a0d43f795fa02c5409a309727965 Mon Sep 17 00:00:00 2001 From: Nutchanon Taechasuk <58178159+Quatton@users.noreply.github.com> Date: Wed, 22 Apr 2026 01:22:59 +0900 Subject: [PATCH 02/23] fix: make unstable_mask optional in Location (#14995) --- contributors.yml | 1 + .../.changes/patch.made-unstablemask-optional-in-location.md | 1 + packages/react-router/lib/router/history.ts | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 packages/react-router/.changes/patch.made-unstablemask-optional-in-location.md diff --git a/contributors.yml b/contributors.yml index 1c035b6794..83d7cbc515 100644 --- a/contributors.yml +++ b/contributors.yml @@ -349,6 +349,7 @@ - pwdcd - pyitphyoaung - qu0b +- Quatton - QzCurious - raphaelbronsveld - redabacha diff --git a/packages/react-router/.changes/patch.made-unstablemask-optional-in-location.md b/packages/react-router/.changes/patch.made-unstablemask-optional-in-location.md new file mode 100644 index 0000000000..01cca2e592 --- /dev/null +++ b/packages/react-router/.changes/patch.made-unstablemask-optional-in-location.md @@ -0,0 +1 @@ +Mark `unstable_mask` as an optional field in `Location` for easier mocking in unit tests diff --git a/packages/react-router/lib/router/history.ts b/packages/react-router/lib/router/history.ts index 301522fcfd..9dd10ab943 100644 --- a/packages/react-router/lib/router/history.ts +++ b/packages/react-router/lib/router/history.ts @@ -74,7 +74,7 @@ export interface Location extends Path { * The masked location displayed in the URL bar, which differs from the URL the * router is operating on */ - unstable_mask: Path | undefined; + unstable_mask?: Path; } /** From 0ff8854a075c74d3b651988dd10000da8dc8952f Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 22 Apr 2026 05:59:42 -0400 Subject: [PATCH 03/23] Remove temp restriction for change file workflow --- .github/workflows/changes-file.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/changes-file.yml b/.github/workflows/changes-file.yml index 12a9df2c62..032e467b9d 100644 --- a/.github/workflows/changes-file.yml +++ b/.github/workflows/changes-file.yml @@ -13,8 +13,7 @@ concurrency: jobs: check: name: 📝 Change File Check - # TODO: Temporarily only run on my PRs for testing purposes - if: github.repository == 'remix-run/react-router' && github.event.pull_request.user.login == 'brophdawg11' + if: github.repository == 'remix-run/react-router' runs-on: ubuntu-latest permissions: pull-requests: write From 226e355ddd12e23ecfbb555a29717e37789d7b8d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 22 Apr 2026 06:04:01 -0400 Subject: [PATCH 04/23] Add links to comments --- scripts/changes/check-pr.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/scripts/changes/check-pr.ts b/scripts/changes/check-pr.ts index 59bb95e35e..62852d2942 100644 --- a/scripts/changes/check-pr.ts +++ b/scripts/changes/check-pr.ts @@ -23,23 +23,22 @@ const COMMENT_MARKER = ""; const COMMENT_FOUND = `${COMMENT_MARKER} ### ✅ Change File Found -A \`.changes\` file has been found in this PR. Thanks!`; +A [change file](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files) file exists in this PR. Thanks!`; const COMMENT_MISSING = `${COMMENT_MARKER} -### 📝 No Change File Found +### ⚠️ No Change File Found -This PR doesn't include a change file which is used for automated release notes. -If your change affects users, please add one (or more) and commit the generated file(s). +This PR doesn't include a [change file](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files) which is used for automated release notes. +If your change affects users, please add one (or more) change files and commit the generated file(s). \`\`\`sh pnpm run changes:add \`\`\` -> This script requires Node 24+. If you are on a lower version, please [add a file manually](https://reactrouter.com/community/contributing#change-files) +> This script requires Node 24+. If you are on a lower version, please [add a file manually](https://github.com/remix-run/react-router/blob/main/docs/community/contributing.md#change-files) > Not every PR needs a change file — you can skip this step if the change is internal-only -> (tests, tooling, docs)\ -`; +> (tests, tooling, docs)`; // Matches packages/*/.changes/*.md but not .gitkeep const CHANGE_FILE_RE = /^packages\/[^/]+\/\.changes\/[^/]+\.md$/; From cbbc6fc7e8f473639849b564e419a9bb1315a6c0 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 22 Apr 2026 10:05:34 -0400 Subject: [PATCH 05/23] perf: cache flattened/ranked route branches (#14967) --- .changeset/hot-bottles-mix.md | 24 ++++++++ ...-toute-matching-caching-flattenedranked.md | 1 + .../react-router/lib/dom/ssr/fog-of-war.ts | 2 +- packages/react-router/lib/router/router.ts | 61 ++++++++++++++++++- packages/react-router/lib/router/utils.ts | 25 ++++++-- packages/react-router/lib/rsc/browser.tsx | 5 +- .../lib/server-runtime/routeMatching.ts | 39 ++++++++---- .../react-router/lib/server-runtime/routes.ts | 16 ----- .../react-router/lib/server-runtime/server.ts | 61 ++++++++++++------- 9 files changed, 172 insertions(+), 62 deletions(-) create mode 100644 .changeset/hot-bottles-mix.md create mode 100644 packages/react-router/.changes/patch.optimize-serverside-toute-matching-caching-flattenedranked.md diff --git a/.changeset/hot-bottles-mix.md b/.changeset/hot-bottles-mix.md new file mode 100644 index 0000000000..6b39e01694 --- /dev/null +++ b/.changeset/hot-bottles-mix.md @@ -0,0 +1,24 @@ +--- +"react-router": patch +--- + +Improve server-side route matching performance by pre-computing flattened/cached route branches + +- Performance benchmark + - 100 route app, 3000 requests @ 10x concurrency across 38 distinct paths + - `dev` branch + - Throughput: 826.1 req/s + - Latency mean: 10.227ms + - Latency p50: 11.125ms + - Latency p95: 13.056ms + - Latency p99: 16.753ms + - Latency min: 1.739ms + - Latency max: 20.268ms + - This branch + - Throughput: 952.7 req/s (15.3% improvement) + - Latency mean: 8.716ms (14.8% improvement) + - Latency p50: 9.452ms + - Latency p95: 11.610ms + - Latency p99: 12.544ms + - Latency min: 1.656ms + - Latency max: 15.936ms diff --git a/packages/react-router/.changes/patch.optimize-serverside-toute-matching-caching-flattenedranked.md b/packages/react-router/.changes/patch.optimize-serverside-toute-matching-caching-flattenedranked.md new file mode 100644 index 0000000000..7ca17d4b24 --- /dev/null +++ b/packages/react-router/.changes/patch.optimize-serverside-toute-matching-caching-flattenedranked.md @@ -0,0 +1 @@ +Cache flattened/ranked route branches to optimize server-side route matching diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts index 78992dea40..6807cd7e1d 100644 --- a/packages/react-router/lib/dom/ssr/fog-of-war.ts +++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts @@ -26,7 +26,7 @@ const discoveredPaths = new Set(); // 7.5k to come in under the ~8k limit for most browsers // https://stackoverflow.com/a/417184 -const URL_LIMIT = 7680; +export const URL_LIMIT = 7680; export function isFogOfWarEnabled( routeDiscovery: ServerBuild["routeDiscovery"], diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 00cf3b79ac..3ba5ccdef5 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -49,6 +49,7 @@ import type { MiddlewareFunction, MiddlewareNextFunction, PatchRoutesOnNavigationFunction, + RouteBranch, } from "./utils"; import { ErrorResponseImpl, @@ -69,6 +70,7 @@ import { RouterContextProvider, getRoutePattern, removeDoubleSlashes, + flattenAndRankRoutes, } from "./utils"; //////////////////////////////////////////////////////////////////////////////// @@ -451,7 +453,33 @@ export interface StaticHandlerContext { * A StaticHandler instance manages a singular SSR navigation/fetch event */ export interface StaticHandler { + /** + * The set of data routes managed by this handler + */ dataRoutes: DataRouteObject[]; + /** + * @private + * PRIVATE - DO NOT USE + * + * The route branches derived from the data routes, used for internal route + * matching in Framework Mode + */ + _internalRouteBranches: RouteBranch[]; + /** + * Perform a query for a given request - executing all matched route + * loaders/actions. Used for document requests. + * + * @param request The request to query + * @param opts Optional query options + * @param opts.dataStrategy Alternate dataStrategy implementation + * @param opts.filterMatchesToLoad Predicate function to filter which matches should be loaded + * @param opts.generateMiddlewareResponse To enable middleware, provide a function + * to generate a response to bubble back up the middleware chain + * @param opts.requestContext Context object to pass to loaders/actions + * @param opts.skipLoaderErrorBubbling Skip loader error bubbling + * @param opts.skipRevalidation Skip revalidation after action submission + * @param opts.unstable_normalizePath Normalize the request path + */ query( request: Request, opts?: { @@ -471,6 +499,19 @@ export interface StaticHandler { unstable_normalizePath?: (request: Request) => Path; }, ): Promise; + /** + * Perform a query for a specific route. Used for resource requests. + * + * @param request The request to query + * @param opts Optional queryRoute options + * @param opts.dataStrategy Alternate dataStrategy implementation + * @param opts.generateMiddlewareResponse To enable middleware, provide a function + * to generate a response to bubble back up the middleware chain + * @param opts.requestContext Context object to pass to loaders/actions + * @param opts.routeId The ID of the route to query + * @param opts.unstable_normalizePath Normalize the request path + + */ queryRoute( request: Request, opts?: { @@ -3803,6 +3844,9 @@ export function createStaticHandler( undefined, manifest, ); + // Pre-compute flattened/ranked route branches when the flag is enabled. + // Skipped in development mode because routes can be added dynamically (HMR). + let routeBranches = flattenAndRankRoutes(dataRoutes); /** * The query() method is intended for document requests, in which we want to @@ -3845,7 +3889,13 @@ export function createStaticHandler( let normalizePath = unstable_normalizePath || defaultNormalizePath; let method = request.method; let location = createLocation("", normalizePath(request), null, "default"); - let matches = matchRoutes(dataRoutes, location, basename); + let matches = matchRoutesImpl( + dataRoutes, + location, + basename, + false, + routeBranches, + ); requestContext = requestContext != null ? requestContext : new RouterContextProvider(); @@ -4122,7 +4172,13 @@ export function createStaticHandler( let normalizePath = unstable_normalizePath || defaultNormalizePath; let method = request.method; let location = createLocation("", normalizePath(request), null, "default"); - let matches = matchRoutes(dataRoutes, location, basename); + let matches = matchRoutesImpl( + dataRoutes, + location, + basename, + false, + routeBranches, + ); requestContext = requestContext != null ? requestContext : new RouterContextProvider(); @@ -4698,6 +4754,7 @@ export function createStaticHandler( return { dataRoutes, + _internalRouteBranches: routeBranches, query, queryRoute, }; diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 3d462b0528..52139a7b74 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -1020,6 +1020,7 @@ export function matchRoutesImpl< locationArg: Partial | string, basename: string, allowPartial: boolean, + precomputedBranches?: RouteBranch[], ): RouteMatch[] | null { let location = typeof locationArg === "string" ? parsePath(locationArg) : locationArg; @@ -1030,10 +1031,10 @@ export function matchRoutesImpl< return null; } - let branches = flattenRoutes(routes); - rankRouteBranches(branches); + let branches = precomputedBranches ?? flattenAndRankRoutes(routes); let matches = null; + let decoded = decodePath(pathname); for (let i = 0; matches == null && i < branches.length; ++i) { // Incoming pathnames are generally encoded from either window.location // or from router.navigate, but we want to match against the unencoded @@ -1041,7 +1042,6 @@ export function matchRoutesImpl< // encoded here but there also shouldn't be anything to decode so this // should be a safe operation. This avoids needing matchRoutes to be // history-aware. - let decoded = decodePath(pathname); matches = matchRouteBranch( branches[i], decoded, @@ -1102,12 +1102,29 @@ interface RouteMeta { route: RouteObjectType; } -interface RouteBranch { +/** + * @private + * PRIVATE - DO NOT USE + * + * A "branch" of routes that match a given route pattern. + * This is an internal interface not intended for direct external usage. + */ +export interface RouteBranch< + RouteObjectType extends RouteObject = RouteObject, +> { path: string; score: number; routesMeta: RouteMeta[]; } +export function flattenAndRankRoutes< + RouteObjectType extends RouteObject = RouteObject, +>(routes: RouteObjectType[]): RouteBranch[] { + let branches = flattenRoutes(routes); + rankRouteBranches(branches); + return branches; +} + function flattenRoutes( routes: RouteObjectType[], branches: RouteBranch[] = [], diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index 9bb06cb06d..961505e672 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -43,6 +43,7 @@ import { import { RSCRouterGlobalErrorBoundary } from "./errorBoundaries"; import type { RouteModules } from "../dom/ssr/routeModules"; import { populateRSCRouteModules } from "./route-modules"; +import { URL_LIMIT } from "../dom/ssr/fog-of-war"; const defaultManifestPath = "/__manifest"; @@ -1018,10 +1019,6 @@ const nextPaths = new Set(); const discoveredPathsMaxSize = 1000; const discoveredPaths = new Set(); -// 7.5k to come in under the ~8k limit for most browsers -// https://stackoverflow.com/a/417184 -const URL_LIMIT = 7680; - function getManifestUrl(paths: string[]): URL | null { if (paths.length === 0) { return null; diff --git a/packages/react-router/lib/server-runtime/routeMatching.ts b/packages/react-router/lib/server-runtime/routeMatching.ts index c0f105c464..76b5f89aff 100644 --- a/packages/react-router/lib/server-runtime/routeMatching.ts +++ b/packages/react-router/lib/server-runtime/routeMatching.ts @@ -1,6 +1,8 @@ -import type { Params, RouteObject } from "../router/utils"; -import { matchRoutes } from "../router/utils"; -import type { ServerRoute } from "./routes"; +import type { DataRouteObject, Params, RouteObject } from "../router/utils"; +import type { RouteBranch } from "../router/utils"; +import { matchRoutesImpl } from "../router/utils"; +import invariant from "./invariant"; +import type { ServerRoute, ServerRouteManifest } from "./routes"; export interface RouteMatch { params: Params; @@ -9,20 +11,31 @@ export interface RouteMatch { } export function matchServerRoutes( - routes: ServerRoute[], + manifest: ServerRouteManifest, + dataRoutes: DataRouteObject[], + branches: RouteBranch[], pathname: string, basename?: string, -): RouteMatch[] | null { - let matches = matchRoutes( - routes as unknown as RouteObject[], +): RouteMatch>[] | null { + let matches = matchRoutesImpl( + dataRoutes, pathname, - basename, + basename ?? "/", + false, + branches, ); if (!matches) return null; - return matches.map((match) => ({ - params: match.params, - pathname: match.pathname, - route: match.route as unknown as ServerRoute, - })); + return matches.map((match) => { + let route = manifest[match.route.id]; + invariant( + route, + `Route with id "${match.route.id}" not found in manifest.`, + ); + return { + params: match.params, + pathname: match.pathname, + route, + }; + }); } diff --git a/packages/react-router/lib/server-runtime/routes.ts b/packages/react-router/lib/server-runtime/routes.ts index 9eebd42011..9ede753d7b 100644 --- a/packages/react-router/lib/server-runtime/routes.ts +++ b/packages/react-router/lib/server-runtime/routes.ts @@ -44,22 +44,6 @@ function groupRoutesByParentId(manifest: ServerRouteManifest) { return routes; } -// Create a map of routes by parentId to use recursively instead of -// repeatedly filtering the manifest. -export function createRoutes( - manifest: ServerRouteManifest, - parentId: string = "", - routesByParentId: Record< - string, - Omit[] - > = groupRoutesByParentId(manifest), -): ServerRoute[] { - return (routesByParentId[parentId] || []).map((route) => ({ - ...route, - children: createRoutes(manifest, route.id, routesByParentId), - })); -} - // Convert the Remix ServerManifest into DataRouteObject's for use with // createStaticHandler export function createStaticHandlerDataRoutes( diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts index 12f249210e..73de96e58d 100644 --- a/packages/react-router/lib/server-runtime/server.ts +++ b/packages/react-router/lib/server-runtime/server.ts @@ -1,9 +1,13 @@ import type { StaticHandler, StaticHandlerContext } from "../router/router"; -import type { ErrorResponse } from "../router/utils"; -import { RouterContextProvider } from "../router/utils"; +import type { + DataRouteObject, + ErrorResponse, + RouteBranch, +} from "../router/utils"; import { isRouteErrorResponse, ErrorResponseImpl, + RouterContextProvider, stripBasename, } from "../router/utils"; import { @@ -21,7 +25,7 @@ import { ServerMode, isServerMode } from "./mode"; import type { RouteMatch } from "./routeMatching"; import { matchServerRoutes } from "./routeMatching"; import type { ServerRoute } from "./routes"; -import { createStaticHandlerDataRoutes, createRoutes } from "./routes"; +import { createStaticHandlerDataRoutes } from "./routes"; import type { ServerHandoff } from "./serverHandoff"; import { createServerHandoffString } from "./serverHandoff"; import { getBuildTimeHeader, getDevServerHooks } from "./dev"; @@ -35,7 +39,7 @@ import { import { getDocumentHeaders } from "./headers"; import type { EntryRoute } from "../dom/ssr/routes"; import type { MiddlewareEnabled } from "../types/future"; -import { getManifestPath } from "../dom/ssr/fog-of-war"; +import { URL_LIMIT, getManifestPath } from "../dom/ssr/fog-of-war"; import type { unstable_InstrumentRequestHandlerFunction } from "../router/instrumentation"; import { instrumentHandler } from "../router/instrumentation"; import { throwIfPotentialCSRFAttack } from "../actions"; @@ -54,12 +58,12 @@ export type CreateRequestHandlerFunction = ( ) => RequestHandler; 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, { basename: build.basename, unstable_instrumentations: build.entry.module.unstable_instrumentations, + future: build.future, }); let errorHandler = @@ -188,7 +192,12 @@ function derive(build: ServerBuild, mode?: string) { ); if (requestUrl.pathname === manifestUrl) { try { - let res = await handleManifestRequest(build, routes, requestUrl); + let res = await handleManifestRequest( + build, + staticHandler.dataRoutes, + staticHandler._internalRouteBranches, + requestUrl, + ); return res; } catch (e) { handleError(e); @@ -196,19 +205,19 @@ function derive(build: ServerBuild, mode?: string) { } } - let matches = matchServerRoutes(routes, normalizedPathname, build.basename); + let matches = matchServerRoutes( + build.routes, + staticHandler.dataRoutes, + staticHandler._internalRouteBranches, + normalizedPathname, + build.basename, + ); if (matches && matches.length > 0) { Object.assign(params, matches[0].params); } let response: Response; if (requestUrl.pathname.endsWith(".data")) { - let singleFetchMatches = matchServerRoutes( - routes, - normalizedPathname, - build.basename, - ); - response = await handleSingleFetchRequest( serverMode, build, @@ -231,7 +240,7 @@ function derive(build: ServerBuild, mode?: string) { if (build.entry.module.handleDataRequest) { response = await build.entry.module.handleDataRequest(response, { context: loadContext, - params: singleFetchMatches ? singleFetchMatches[0].params : {}, + params: matches ? matches[0].params : {}, request, }); @@ -305,8 +314,6 @@ function derive(build: ServerBuild, mode?: string) { } return { - routes, - dataRoutes, serverMode, staticHandler, errorHandler, @@ -319,7 +326,6 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( mode, ) => { let _build: ServerBuild; - let routes: ServerRoute[]; let serverMode: ServerMode; let staticHandler: StaticHandler; let errorHandler: HandleErrorFunction; @@ -330,20 +336,17 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( if (typeof build === "function") { let derived = derive(_build, mode); - routes = derived.routes; serverMode = derived.serverMode; staticHandler = derived.staticHandler; errorHandler = derived.errorHandler; _requestHandler = derived.requestHandler; } else if ( - !routes || !serverMode || !staticHandler || !errorHandler || !_requestHandler ) { let derived = derive(_build, mode); - routes = derived.routes; serverMode = derived.serverMode; staticHandler = derived.staticHandler; errorHandler = derived.errorHandler; @@ -356,7 +359,8 @@ export const createRequestHandler: CreateRequestHandlerFunction = ( async function handleManifestRequest( build: ServerBuild, - routes: ServerRoute[], + dataRoutes: DataRouteObject[], + branches: RouteBranch[], url: URL, ) { if (build.assets.version !== url.searchParams.get("version")) { @@ -368,6 +372,13 @@ async function handleManifestRequest( }); } + if (url.toString().length > URL_LIMIT) { + return new Response(null, { + statusText: "Bad Request", + status: 400, + }); + } + let patches: Record = {}; if (url.searchParams.has("paths")) { @@ -395,7 +406,13 @@ async function handleManifestRequest( }); for (let path of paths) { - let matches = matchServerRoutes(routes, path, build.basename); + let matches = matchServerRoutes( + build.routes, + dataRoutes, + branches, + path, + build.basename, + ); if (matches) { for (let match of matches) { let routeId = match.route.id; From 9e9b622f7233308f3479f872aa669e1128a3ec31 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Wed, 22 Apr 2026 11:27:41 -0400 Subject: [PATCH 06/23] Update pnpm/actions-setuip to v6 --- .github/workflows/changes-file.yml | 2 +- .github/workflows/close-no-repro-issues.yml | 2 +- .github/workflows/deduplicate-lock-file.yml | 2 +- .github/workflows/delete-changeset-bot-comments.yml | 2 +- .github/workflows/docs.yml | 2 +- .github/workflows/format.yml | 2 +- .github/workflows/preview.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/shared-build.yml | 2 +- .github/workflows/shared-integration.yml | 2 +- .github/workflows/test.yml | 2 +- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/changes-file.yml b/.github/workflows/changes-file.yml index 032e467b9d..7bbe0580c2 100644 --- a/.github/workflows/changes-file.yml +++ b/.github/workflows/changes-file.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v6 - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/close-no-repro-issues.yml b/.github/workflows/close-no-repro-issues.yml index 46683ee8c4..98337a2d38 100644 --- a/.github/workflows/close-no-repro-issues.yml +++ b/.github/workflows/close-no-repro-issues.yml @@ -28,7 +28,7 @@ jobs: uses: actions/checkout@v6 - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4.1.0 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/deduplicate-lock-file.yml b/.github/workflows/deduplicate-lock-file.yml index 654dd49ba9..ae862582c8 100644 --- a/.github/workflows/deduplicate-lock-file.yml +++ b/.github/workflows/deduplicate-lock-file.yml @@ -23,7 +23,7 @@ jobs: token: ${{ secrets.FORMAT_PAT }} - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/delete-changeset-bot-comments.yml b/.github/workflows/delete-changeset-bot-comments.yml index 32536b5065..255c20ea91 100644 --- a/.github/workflows/delete-changeset-bot-comments.yml +++ b/.github/workflows/delete-changeset-bot-comments.yml @@ -16,7 +16,7 @@ jobs: uses: actions/checkout@v6 - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0c819d3b85..cb4e2ad2c5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -32,7 +32,7 @@ jobs: ref: ${{ github.event.inputs.branch }} - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index d3dddb77d9..490c785090 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -22,7 +22,7 @@ jobs: token: ${{ secrets.FORMAT_PAT }} - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 1e3c2ce6e1..c78af646d0 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -56,7 +56,7 @@ jobs: ref: ${{ inputs.baseBranch }} - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: Install Node.js uses: actions/setup-node@v6 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 76e4153f51..4c8cb7e2d4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -93,7 +93,7 @@ jobs: uses: actions/checkout@v6 - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 @@ -157,7 +157,7 @@ jobs: fetch-depth: 0 - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/shared-build.yml b/.github/workflows/shared-build.yml index c24e03d205..06e5e44b48 100644 --- a/.github/workflows/shared-build.yml +++ b/.github/workflows/shared-build.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v6 - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 diff --git a/.github/workflows/shared-integration.yml b/.github/workflows/shared-integration.yml index 2d75cd43aa..34fade4888 100644 --- a/.github/workflows/shared-integration.yml +++ b/.github/workflows/shared-integration.yml @@ -41,7 +41,7 @@ jobs: uses: actions/checkout@v6 - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node ${{ matrix.node }} uses: actions/setup-node@v6 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6470a361c5..830e6120be 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,7 +36,7 @@ jobs: uses: actions/checkout@v6 - name: 📦 Setup pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@v6 - name: ⎔ Setup node uses: actions/setup-node@v6 From 0e495d5de895f59e2f1a8f526a61ba85ad14b048 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 28 Apr 2026 08:52:31 -0400 Subject: [PATCH 07/23] Add issues:write permission to change file check workflow --- .github/workflows/changes-file.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/changes-file.yml b/.github/workflows/changes-file.yml index 7bbe0580c2..76098443a1 100644 --- a/.github/workflows/changes-file.yml +++ b/.github/workflows/changes-file.yml @@ -16,6 +16,7 @@ jobs: if: github.repository == 'remix-run/react-router' runs-on: ubuntu-latest permissions: + issues: write pull-requests: write steps: From 10a968671d94a1147d78e766cffae33c6d151162 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 28 Apr 2026 15:52:35 -0400 Subject: [PATCH 08/23] Migrate changeset to change file --- .../react-router/.changes/patch.hot-bottles-mix.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) rename .changeset/hot-bottles-mix.md => packages/react-router/.changes/patch.hot-bottles-mix.md (86%) diff --git a/.changeset/hot-bottles-mix.md b/packages/react-router/.changes/patch.hot-bottles-mix.md similarity index 86% rename from .changeset/hot-bottles-mix.md rename to packages/react-router/.changes/patch.hot-bottles-mix.md index 6b39e01694..7c9a62a357 100644 --- a/.changeset/hot-bottles-mix.md +++ b/packages/react-router/.changes/patch.hot-bottles-mix.md @@ -1,8 +1,4 @@ ---- -"react-router": patch ---- - -Improve server-side route matching performance by pre-computing flattened/cached route branches +Improve server-side route matching performance by pre-computing flattened/cached route branches ([#14967](https://github.com/remix-run/react-router/pull/14967)) - Performance benchmark - 100 route app, 3000 requests @ 10x concurrency across 38 distinct paths From 6f18edde6b5eac5f7028bfde885bbb8031025480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abdulwahaab=20Ahmed=20=F0=9F=8D=89?= Date: Wed, 29 Apr 2026 10:24:57 -0400 Subject: [PATCH 09/23] Add nonce to scripts `modulepreload` (#15002) --- contributors.yml | 1 + integration/sri-test.ts | 13 ++++ .../patch.add-nonce-scripts-modulepreload.md | 1 + .../__tests__/dom/ssr/components-test.tsx | 78 +++++++++++++++++++ .../react-router/lib/dom/ssr/components.tsx | 3 + 5 files changed, 96 insertions(+) create mode 100644 packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md diff --git a/contributors.yml b/contributors.yml index 83d7cbc515..01178019f1 100644 --- a/contributors.yml +++ b/contributors.yml @@ -3,6 +3,7 @@ - 43081j - aarbi - abdallah-nour +- abdulwahaab710 - abeadam - abhi-kr-2100 - abhijeetpandit7 diff --git a/integration/sri-test.ts b/integration/sri-test.ts index 7e45bad97a..1178994aff 100644 --- a/integration/sri-test.ts +++ b/integration/sri-test.ts @@ -71,4 +71,17 @@ test.describe("CSub-Resource Integrity", () => { await page.locator('script[type="importmap"]').getAttribute("nonce"), ).toBe("test-nonce-123"); }); + + test("includes a nonce on modulepreload links", async ({ page }) => { + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); + let modulePreloads = page.locator('link[rel="modulepreload"]'); + let count = await modulePreloads.count(); + expect(count).toBeGreaterThan(0); + for (let i = 0; i < count; i++) { + expect(await modulePreloads.nth(i).getAttribute("nonce")).toBe( + "test-nonce-123", + ); + } + }); }); diff --git a/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md b/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md new file mode 100644 index 0000000000..ba4ab6112e --- /dev/null +++ b/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md @@ -0,0 +1 @@ +Add nonce to scripts modulepreload diff --git a/packages/react-router/__tests__/dom/ssr/components-test.tsx b/packages/react-router/__tests__/dom/ssr/components-test.tsx index f13a6b561a..3b4a0a1e36 100644 --- a/packages/react-router/__tests__/dom/ssr/components-test.tsx +++ b/packages/react-router/__tests__/dom/ssr/components-test.tsx @@ -390,6 +390,84 @@ describe("", () => { }); }); +describe("", () => { + it("propagates nonce to modulepreload links", async () => { + let staticHandlerContext = await createStaticHandler([{ path: "/" }]).query( + new Request("http://localhost/"), + ); + + invariant( + !(staticHandlerContext instanceof Response), + "Expected a context", + ); + + let context = mockEntryContext({ + manifest: { + routes: { + root: { + hasLoader: false, + hasAction: false, + hasErrorBoundary: false, + id: "root", + module: "root.js", + path: "/", + }, + }, + entry: { + imports: ["preload-a.js", "preload-b.js"], + module: "entry.js", + }, + url: "manifest.js", + version: "", + }, + routeDiscovery: { mode: "initial", manifestPath: "/__manifest" }, + routeModules: { + root: { + default: () => ( + <> +

Root

+ + + ), + }, + }, + }); + + let { container } = render( + , + ); + + let modulePreloads = container.ownerDocument.querySelectorAll( + 'link[rel="modulepreload"]', + ); + expect(modulePreloads.length).toBeGreaterThan(0); + modulePreloads.forEach((link) => { + expect(link).toHaveAttribute("nonce", "test-nonce"); + }); + + expect( + container.ownerDocument.querySelector( + 'link[rel="modulepreload"][href="manifest.js"]', + ), + ).toHaveAttribute("nonce", "test-nonce"); + expect( + container.ownerDocument.querySelector( + 'link[rel="modulepreload"][href="entry.js"]', + ), + ).toHaveAttribute("nonce", "test-nonce"); + expect( + container.ownerDocument.querySelector( + 'link[rel="modulepreload"][href="preload-a.js"]', + ), + ).toHaveAttribute("nonce", "test-nonce"); + expect( + container.ownerDocument.querySelector( + 'link[rel="modulepreload"][href="preload-b.js"]', + ), + ).toHaveAttribute("nonce", "test-nonce"); + }); +}); + describe("usePrefetchBehavior", () => { function TestComponent({ prefetch, diff --git a/packages/react-router/lib/dom/ssr/components.tsx b/packages/react-router/lib/dom/ssr/components.tsx index 00cf26cb50..78f362796a 100644 --- a/packages/react-router/lib/dom/ssr/components.tsx +++ b/packages/react-router/lib/dom/ssr/components.tsx @@ -1011,6 +1011,7 @@ import(${JSON.stringify(manifest.entry.module)});`; href={manifest.url} crossOrigin={scriptProps.crossOrigin} integrity={sri[manifest.url]} + nonce={scriptProps.nonce} suppressHydrationWarning /> ) : null} @@ -1019,6 +1020,7 @@ import(${JSON.stringify(manifest.entry.module)});`; href={manifest.entry.module} crossOrigin={scriptProps.crossOrigin} integrity={sri[manifest.entry.module]} + nonce={scriptProps.nonce} suppressHydrationWarning /> {preloads.map((path) => ( @@ -1028,6 +1030,7 @@ import(${JSON.stringify(manifest.entry.module)});`; href={path} crossOrigin={scriptProps.crossOrigin} integrity={sri[path]} + nonce={scriptProps.nonce} suppressHydrationWarning /> ))} From 67518cb61054f1b3eede95dde95480aca962626d Mon Sep 17 00:00:00 2001 From: dadamssg Date: Mon, 4 May 2026 10:51:04 -0500 Subject: [PATCH 10/23] Remove unnecessary hasShouldRevalidate condition for opting out (#15012) --- integration/single-fetch-test.ts | 90 +++++++++++++++++++ ...faultshouldrevalidatefalse-used-matched.md | 1 + .../react-router/lib/dom/ssr/single-fetch.tsx | 9 +- packages/react-router/lib/rsc/browser.tsx | 4 - 4 files changed, 93 insertions(+), 11 deletions(-) create mode 100644 packages/react-router/.changes/patch.fixes-bug-when-unstabledefaultshouldrevalidatefalse-used-matched.md diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index 81c4c2e52d..dbb23f4ec2 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -1063,6 +1063,96 @@ test.describe("single-fetch", () => { expect(urls).toEqual([expect.stringMatching(/\/action\.data$/)]); }); + test("call-site revalidation opt-out handles parent routes w/o shouldRevalidate", async ({ + page, + }) => { + let fixture = await createFixture({ + files: { + "app/root.tsx": js` + import { Link, Links, Meta, Outlet, Scripts, useMatches } from "react-router"; + + let count = 0 + + export function loader() { + return { count: ++count }; + } + + export default function Root() { + return ( + + + + + + + +
+                    {JSON.stringify(useMatches().map(m => [m.id, m.data]))}
+                  
+ + + + + ); + } + `, + "app/routes/page.tsx": js` + let count = 0 + + export function loader() { + return { count: ++count } + } + + export default function Component() { + return

Page

+ } + `, + }, + }); + + let urls: string[] = []; + page.on("request", (req) => { + if (req.url().includes(".data")) { + let url = new URL(req.url()); + urls.push(url.pathname + url.search); + } + }); + + console.error = () => {}; + + let appFixture = await createAppFixture(fixture); + let app = new PlaywrightFixture(appFixture, page); + await app.goto("/"); // root increments to 1 + expect(await page.locator("#data").innerText()).toBe( + '[["root",{"count":1}],["routes/_index",null]]', + ); + + await app.clickLink("/page"); // root increments to 2 + expect(await page.locator("#data").innerText()).toBe( + '[["root",{"count":2}],["routes/page",{"count":1}]]', + ); + expect(urls).toEqual(["/page.data"]); + urls.splice(0, urls.length); + + await app.clickLink("/"); // root increments to 3 + expect(await page.locator("#data").innerText()).toBe( + '[["root",{"count":3}],["routes/_index",null]]', + ); + expect(urls).toEqual(["/_root.data"]); + urls.splice(0, urls.length); + + await app.clickLink("/page?optout"); + // root stays at 3, page is a fresh load so increments to 2 + expect(await page.locator("#data").innerText()).toBe( + '[["root",{"count":3}],["routes/page",{"count":2}]]', + ); + expect(urls).toEqual(["/page.data?optout=&_routes=routes%2Fpage"]); + }); + test("returns headers correctly for singular loader and action calls", async () => { let fixture = await createFixture({ files: { diff --git a/packages/react-router/.changes/patch.fixes-bug-when-unstabledefaultshouldrevalidatefalse-used-matched.md b/packages/react-router/.changes/patch.fixes-bug-when-unstabledefaultshouldrevalidatefalse-used-matched.md new file mode 100644 index 0000000000..d951f77691 --- /dev/null +++ b/packages/react-router/.changes/patch.fixes-bug-when-unstabledefaultshouldrevalidatefalse-used-matched.md @@ -0,0 +1 @@ +Fix a bug with `unstable_defaultShouldRevalidate={false}` where parent routes that did not export a `shouldRevalidate` function could be incorrectly included in the single fetch call for new child route data diff --git a/packages/react-router/lib/dom/ssr/single-fetch.tsx b/packages/react-router/lib/dom/ssr/single-fetch.tsx index 6e02210f4a..eff82ebc07 100644 --- a/packages/react-router/lib/dom/ssr/single-fetch.tsx +++ b/packages/react-router/lib/dom/ssr/single-fetch.tsx @@ -166,7 +166,6 @@ export function StreamTransfer({ type GetRouteInfoFunction = (match: DataRouteMatch) => { hasLoader: boolean; hasClientLoader: boolean; - hasShouldRevalidate: boolean; }; type ShouldAllowOptOutFunction = (match: DataRouteMatch) => boolean; @@ -192,11 +191,9 @@ export function getTurboStreamSingleFetchDataStrategy( (match: DataRouteMatch) => { let manifestRoute = manifest.routes[match.route.id]; invariant(manifestRoute, "Route not found in manifest"); - let routeModule = routeModules[match.route.id]; return { hasLoader: manifestRoute.hasLoader, hasClientLoader: manifestRoute.hasClientLoader, - hasShouldRevalidate: Boolean(routeModule?.shouldRevalidate), }; }, fetchAndDecodeViaTurboStream, @@ -415,8 +412,7 @@ async function singleFetchLoaderNavigationStrategy( m.resolve(async (handler) => { routeDfds[i].resolve(); let routeId = m.route.id; - let { hasLoader, hasClientLoader, hasShouldRevalidate } = - getRouteInfo(m); + let { hasLoader, hasClientLoader } = getRouteInfo(m); let defaultShouldRevalidate = !m.shouldRevalidateArgs || @@ -428,8 +424,7 @@ async function singleFetchLoaderNavigationStrategy( // If this route opted out, don't include in the .data request foundOptOutRoute ||= m.shouldRevalidateArgs != null && // This is a revalidation, - hasLoader && // for a route with a server loader, - hasShouldRevalidate === true; // and a shouldRevalidate function + hasLoader; // for a route with a server loader return; } diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index 961505e672..f7ff676a03 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -460,7 +460,6 @@ export function getRSCSingleFetchDataStrategy( hasComponent: boolean; hasAction: boolean; hasClientAction: boolean; - hasShouldRevalidate: boolean; }; }; @@ -475,7 +474,6 @@ export function getRSCSingleFetchDataStrategy( hasComponent: M.route.hasComponent, hasAction: M.route.hasAction, hasClientAction: M.route.hasClientAction, - hasShouldRevalidate: M.route.hasShouldRevalidate, }; }, // pass map into fetchAndDecode so it can add payloads @@ -889,7 +887,6 @@ type DataRouteObjectWithManifestInfo = DataRouteObject & { hasClientLoader: boolean; hasAction: boolean; hasClientAction: boolean; - hasShouldRevalidate: boolean; }; function createRouteFromServerManifest( @@ -976,7 +973,6 @@ function createRouteFromServerManifest( hasClientLoader: match.clientLoader != null, hasAction: match.hasAction, hasClientAction: match.clientAction != null, - hasShouldRevalidate: match.shouldRevalidate != null, }; if (typeof dataRoute.loader === "function") { From 5f61543c31c442271162b31ee9fe1499e170c68c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 4 May 2026 12:47:11 -0400 Subject: [PATCH 11/23] Client-side route matching optimizations (#14971) * Client-side route matching optimizations * Update field name --- .changeset/tough-needles-doubt.md | 27 ++ packages/react-router/lib/components.tsx | 12 +- packages/react-router/lib/dom/server.tsx | 16 +- packages/react-router/lib/dom/ssr/entry.ts | 7 +- .../react-router/lib/dom/ssr/fog-of-war.ts | 15 +- packages/react-router/lib/dom/ssr/server.tsx | 4 +- packages/react-router/lib/hooks.tsx | 13 +- packages/react-router/lib/router/router.ts | 234 +++++++++++++----- .../react-router/lib/server-runtime/server.ts | 1 + 9 files changed, 262 insertions(+), 67 deletions(-) create mode 100644 .changeset/tough-needles-doubt.md diff --git a/.changeset/tough-needles-doubt.md b/.changeset/tough-needles-doubt.md new file mode 100644 index 0000000000..fd0598f784 --- /dev/null +++ b/.changeset/tough-needles-doubt.md @@ -0,0 +1,27 @@ +--- +"react-router": patch +--- + +Improve route matching performance in Framework/Data Mode + +- Avoiding unnecessary calls to `matchRoutes` in data router scenarios + - This includes adding back the optimization that was removed in `7.6.0` ([#13562](https://github.com/remix-run/react-router/pull/13562)) + - The issues that prompted the revert have been addressed by using the available router `matches` but always updating `match.route` to the latest route in the `manifest` +- Leverage pre-computed pre-computing flattened/cached route branches during client side route matching +- This builds on top of prior server optimizations and provides an additional set of gains (~30%): + - Original server optimizations branch + - Throughput: 952.7 req/s + - Latency mean: 8.716ms + - Latency p50: 9.452ms + - Latency p95: 11.610ms + - Latency p99: 12.544ms + - Latency min: 1.656ms + - Latency max: 15.936ms + - This branch + - Throughput: 1235.3 req/s + - Latency mean: 6.095ms + - Latency p50: 6.655ms + - Latency p95: 8.327ms + - Latency p99: 12.133ms + - Latency min: 1.066ms + - Latency max: 19.056ms diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 84832b3927..59be609ead 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -31,6 +31,7 @@ import type { NonIndexRouteObject, Params, PatchRoutesOnNavigationFunction, + RouteManifest, RouteMatch, RouteObject, TrackedPromise, @@ -756,6 +757,7 @@ export function RouterProvider({ > []; future?: Partial; } = {}, ): DataRouter { @@ -435,6 +443,12 @@ export function createStaticRouter( get routes() { return dataRoutes; }, + get branches() { + return opts.branches; + }, + get manifest() { + return manifest; + }, get window() { return undefined; }, diff --git a/packages/react-router/lib/dom/ssr/entry.ts b/packages/react-router/lib/dom/ssr/entry.ts index c8eaf06c74..a99b24cfa3 100644 --- a/packages/react-router/lib/dom/ssr/entry.ts +++ b/packages/react-router/lib/dom/ssr/entry.ts @@ -2,7 +2,11 @@ import type { StaticHandlerContext } from "../../router/router"; import type { EntryRoute } from "./routes"; import type { RouteModules } from "./routeModules"; -import type { RouteManifest } from "../../router/utils"; +import type { + DataRouteObject, + RouteBranch, + RouteManifest, +} from "../../router/utils"; import type { ServerBuild } from "../../server-runtime/build"; type SerializedError = { @@ -39,6 +43,7 @@ export interface FrameworkContextObject { // Additional React-Router information needed at runtime, but not hydrated // through RemixContext export interface EntryContext extends FrameworkContextObject { + branches: RouteBranch[]; staticHandlerContext: StaticHandlerContext; serverHandoffStream?: ReadableStream; } diff --git a/packages/react-router/lib/dom/ssr/fog-of-war.ts b/packages/react-router/lib/dom/ssr/fog-of-war.ts index 6807cd7e1d..35d117887b 100644 --- a/packages/react-router/lib/dom/ssr/fog-of-war.ts +++ b/packages/react-router/lib/dom/ssr/fog-of-war.ts @@ -1,14 +1,11 @@ import * as React from "react"; import type { Router as DataRouter } from "../../router/router"; import type { + DataRouteObject, PatchRoutesOnNavigationFunction, RouteManifest, } from "../../router/utils"; -import { - joinPaths, - matchRoutes, - removeDoubleSlashes, -} from "../../router/utils"; +import { joinPaths, matchRoutesImpl } from "../../router/utils"; import type { AssetsManifest } from "./entry"; import type { RouteModules } from "./routeModules"; import type { EntryRoute } from "./routes"; @@ -56,7 +53,13 @@ export function getPartialManifest( } paths.forEach((path) => { - let matches = matchRoutes(router.routes, path, router.basename); + let matches = matchRoutesImpl( + router.routes, + path, + router.basename || "/", + false, + router.branches, + ); if (matches) { matches.forEach((m) => routeIds.add(m.route.id)); } diff --git a/packages/react-router/lib/dom/ssr/server.tsx b/packages/react-router/lib/dom/ssr/server.tsx index ce0fcf0bc2..953f4a03a1 100644 --- a/packages/react-router/lib/dom/ssr/server.tsx +++ b/packages/react-router/lib/dom/ssr/server.tsx @@ -90,7 +90,9 @@ export function ServerRouter({ } } - let router = createStaticRouter(routes, context.staticHandlerContext); + let router = createStaticRouter(routes, context.staticHandlerContext, { + branches: context.branches, + }); return ( <> diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index 21c10d5dd6..ea5fa50c4b 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -33,6 +33,7 @@ import type { Params, PathMatch, PathPattern, + RouteManifest, RouteMatch, RouteObject, UIMatch, @@ -760,6 +761,7 @@ export function useRoutesImpl( routes: RouteObject[], locationArg?: Partial | string, dataRouterOpts?: { + manifest: RouteManifest; state: DataRouter["state"]; isStatic: boolean; onError: ClientOnErrorFunction | undefined; @@ -860,7 +862,16 @@ export function useRoutesImpl( remainingPathname = "/" + segments.slice(parentSegments.length).join("/"); } - let matches = matchRoutes(routes, { pathname: remainingPathname }); + let matches = + dataRouterOpts && dataRouterOpts.state.matches.length + ? // If we're in a data router, use the matches we've already identified but ensure + // we have the latest route instances from the manifest in case elements have changed + dataRouterOpts.state.matches.map((m) => + Object.assign(m, { + route: dataRouterOpts.manifest[m.route.id] || m.route, + }), + ) + : matchRoutes(routes, { pathname: remainingPathname }); if (ENABLE_DEV_WARNINGS) { warning( diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 3ba5ccdef5..8641b99f32 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -62,7 +62,6 @@ import { isUnsupportedLazyRouteObjectKey, isUnsupportedLazyRouteFunctionKey, isRouteErrorResponse, - matchRoutes, matchRoutesImpl, prependBasename, resolveTo, @@ -113,6 +112,22 @@ export interface Router { */ get routes(): DataRouteObject[]; + /** + * @private + * PRIVATE - DO NOT USE + * + * Return the route branches for this router instance + */ + get branches(): RouteBranch[] | undefined; + + /** + * @private + * PRIVATE - DO NOT USE + * + * Return the manifest for this router instance + */ + get manifest(): RouteManifest; + /** * @private * PRIVATE - DO NOT USE @@ -908,6 +923,63 @@ const ResetLoaderDataSymbol = Symbol("ResetLoaderData"); //#region createRouter //////////////////////////////////////////////////////////////////////////////// +/** + * Encapsulates the stable and in-flight route trees together with their + * pre-computed branch caches so the structures always stay in sync. + */ +class DataRoutes { + #routes: DataRouteObject[]; + #branches: RouteBranch[]; + #hmrRoutes: DataRouteObject[] | undefined; + #hmrBranches: RouteBranch[] | undefined; + + constructor(routes: DataRouteObject[]) { + this.#routes = routes; + this.#branches = flattenAndRankRoutes(routes); + } + + /** The stable route tree */ + get stableRoutes(): DataRouteObject[] { + return this.#routes; + } + + /** The in-flight route tree if one is active, otherwise the stable tree */ + get activeRoutes(): DataRouteObject[] { + return this.#hmrRoutes ?? this.#routes; + } + + /** Pre-computed branches */ + get branches(): RouteBranch[] { + return this.#hmrBranches ?? this.#branches; + } + + get hasHMRRoutes(): boolean { + return this.#hmrRoutes != null; + } + + /** Replace the stable route tree and recompute its branches */ + setRoutes(routes: DataRouteObject[]): void { + this.#routes = routes; + this.#branches = flattenAndRankRoutes(routes); + } + + /** Set a new in-flight route tree and recompute its branches */ + setHmrRoutes(routes: DataRouteObject[]): void { + this.#hmrRoutes = routes; + this.#hmrBranches = flattenAndRankRoutes(routes); + } + + /** Commit in-flight routes/branches to the stable slot and clear in-flight */ + commitHmrRoutes(): void { + if (this.#hmrRoutes) { + this.#routes = this.#hmrRoutes; + this.#branches = this.#hmrBranches!; + this.#hmrRoutes = undefined; + this.#hmrBranches = undefined; + } + } +} + /** * Create a router and listen to history POP navigations */ @@ -952,14 +1024,16 @@ export function createRouter(init: RouterInit): Router { // Routes keyed by ID let manifest: RouteManifest = {}; - // Routes in tree format for matching - let dataRoutes = convertRoutesToDataRoutes( - init.routes, - mapRouteProperties, - undefined, - manifest, + // Route tree, in-flight variant, and their pre-computed ranked branch caches. + // Always updated together via the Routes class to keep them in sync. + let dataRoutes = new DataRoutes( + convertRoutesToDataRoutes( + init.routes, + mapRouteProperties, + undefined, + manifest, + ), ); - let inFlightDataRoutes: DataRouteObject[] | undefined; let basename = init.basename || "/"; if (!basename.startsWith("/")) { basename = `/${basename}`; @@ -971,6 +1045,7 @@ export function createRouter(init: RouterInit): Router { unstable_passThroughRequests: false, ...init.future, }; + // Cleanup function for history let unlistenHistory: (() => void) | null = null; // Externally-provided functions to call on all state changes @@ -989,7 +1064,13 @@ export function createRouter(init: RouterInit): Router { // SSR did the initial scroll restoration. let initialScrollRestored = init.hydrationData != null; - let initialMatches = matchRoutes(dataRoutes, init.history.location, basename); + let initialMatches = matchRoutesImpl( + dataRoutes.activeRoutes, + init.history.location, + basename, + false, + dataRoutes.branches, + ); let initialMatchesIsFOW = false; let initialErrors: RouteData | null = null; let initialized: boolean; @@ -1001,7 +1082,7 @@ export function createRouter(init: RouterInit): Router { let error = getInternalRouterError(404, { pathname: init.history.location.pathname, }); - let { matches, route } = getShortCircuitMatches(dataRoutes); + let { matches, route } = getShortCircuitMatches(dataRoutes.activeRoutes); initialized = true; renderFallback = !initialized; initialMatches = matches; @@ -1016,7 +1097,7 @@ export function createRouter(init: RouterInit): Router { if (initialMatches && !init.hydrationData) { let fogOfWar = checkFogOfWar( initialMatches, - dataRoutes, + dataRoutes.activeRoutes, init.history.location.pathname, ); if (fogOfWar.active) { @@ -1034,7 +1115,7 @@ export function createRouter(init: RouterInit): Router { // the initial matches so we can properly render `HydrateFallback`'s let fogOfWar = checkFogOfWar( null, - dataRoutes, + dataRoutes.activeRoutes, init.history.location.pathname, ); if (fogOfWar.active && fogOfWar.matches) { @@ -1449,10 +1530,7 @@ export function createRouter(init: RouterInit): Router { location.state?._isRedirect !== true); // Commit any in-flight routes at the end of the HMR revalidation "navigation" - if (inFlightDataRoutes) { - dataRoutes = inFlightDataRoutes; - inFlightDataRoutes = undefined; - } + dataRoutes.commitHmrRoutes(); if (isUninterruptedRevalidation) { // If this was an uninterrupted revalidation then do not touch history @@ -1777,7 +1855,7 @@ export function createRouter(init: RouterInit): Router { pendingViewTransitionEnabled = (opts && opts.enableViewTransition) === true; - let routesToUse = inFlightDataRoutes || dataRoutes; + let routesToUse = dataRoutes.activeRoutes; let loadingNavigation = opts && opts.overrideNavigation; let matches = opts?.initialHydration && @@ -1786,7 +1864,13 @@ export function createRouter(init: RouterInit): Router { !initialMatchesIsFOW ? // `matchRoutes()` has already been called if we're in here via `router.initialize()` state.matches - : matchRoutes(routesToUse, location, basename); + : matchRoutesImpl( + routesToUse, + location, + basename, + false, + dataRoutes.branches, + ); let flushSync = (opts && opts.flushSync) === true; // Short circuit if it's only a hash change and not a revalidation or @@ -1978,7 +2062,9 @@ export function createRouter(init: RouterInit): Router { return { shortCircuited: true }; } else if (discoverResult.type === "error") { if (discoverResult.partialMatches.length === 0) { - let { matches, route } = getShortCircuitMatches(dataRoutes); + let { matches, route } = getShortCircuitMatches( + dataRoutes.activeRoutes, + ); return { matches, pendingActionResult: [ @@ -2190,7 +2276,9 @@ export function createRouter(init: RouterInit): Router { return { shortCircuited: true }; } else if (discoverResult.type === "error") { if (discoverResult.partialMatches.length === 0) { - let { matches, route } = getShortCircuitMatches(dataRoutes); + let { matches, route } = getShortCircuitMatches( + dataRoutes.activeRoutes, + ); return { matches, loaderData: {}, @@ -2225,7 +2313,7 @@ export function createRouter(init: RouterInit): Router { } } - let routesToUse = inFlightDataRoutes || dataRoutes; + let routesToUse = dataRoutes.activeRoutes; let { dsMatches, revalidatingFetchers } = getMatchesToLoad( request, scopedContext, @@ -2246,6 +2334,7 @@ export function createRouter(init: RouterInit): Router { routesToUse, basename, init.patchRoutesOnNavigation != null, + dataRoutes.branches, pendingActionResult, callSiteDefaultShouldRevalidate, ); @@ -2436,7 +2525,7 @@ export function createRouter(init: RouterInit): Router { let flushSync = (opts && opts.flushSync) === true; - let routesToUse = inFlightDataRoutes || dataRoutes; + let routesToUse = dataRoutes.activeRoutes; let normalizedPath = normalizeTo( state.location, state.matches, @@ -2445,7 +2534,13 @@ export function createRouter(init: RouterInit): Router { routeId, opts?.relative, ); - let matches = matchRoutes(routesToUse, normalizedPath, basename); + let matches = matchRoutesImpl( + routesToUse, + normalizedPath, + basename, + false, + dataRoutes.branches, + ); let fogOfWar = checkFogOfWar(matches, routesToUse, normalizedPath); if (fogOfWar.active && fogOfWar.matches) { @@ -2666,10 +2761,16 @@ export function createRouter(init: RouterInit): Router { nextLocation, abortController.signal, ); - let routesToUse = inFlightDataRoutes || dataRoutes; + let routesToUse = dataRoutes.activeRoutes; let matches = state.navigation.state !== "idle" - ? matchRoutes(routesToUse, state.navigation.location, basename) + ? matchRoutesImpl( + routesToUse, + state.navigation.location, + basename, + false, + dataRoutes.branches, + ) : state.matches; invariant(matches, "Didn't find any matches after fetcher action"); @@ -2700,6 +2801,7 @@ export function createRouter(init: RouterInit): Router { routesToUse, basename, init.patchRoutesOnNavigation != null, + dataRoutes.branches, [match.route.id, actionResult], callSiteDefaultShouldRevalidate, ); @@ -3465,7 +3567,7 @@ export function createRouter(init: RouterInit): Router { function handleNavigational404(pathname: string) { let error = getInternalRouterError(404, { pathname }); - let routesToUse = inFlightDataRoutes || dataRoutes; + let routesToUse = dataRoutes.activeRoutes; let { matches, route } = getShortCircuitMatches(routesToUse); return { notFoundMatches: matches, route, error }; @@ -3541,12 +3643,14 @@ export function createRouter(init: RouterInit): Router { pathname: string, ): { active: boolean; matches: DataRouteMatch[] | null } { if (init.patchRoutesOnNavigation) { + let activeBranches = dataRoutes.branches; if (!matches) { let fogMatches = matchRoutesImpl( routesToUse, pathname, basename, true, + activeBranches, ); return { active: true, matches: fogMatches || [] }; @@ -3560,6 +3664,7 @@ export function createRouter(init: RouterInit): Router { pathname, basename, true, + activeBranches, ); return { active: true, matches: partialMatches }; } @@ -3596,8 +3701,6 @@ export function createRouter(init: RouterInit): Router { let partialMatches: DataRouteMatch[] | null = matches; while (true) { - let isNonHMR = inFlightDataRoutes == null; - let routesToUse = inFlightDataRoutes || dataRoutes; let localManifest = manifest; try { await init.patchRoutesOnNavigation({ @@ -3610,7 +3713,7 @@ export function createRouter(init: RouterInit): Router { patchRoutesImpl( routeId, children, - routesToUse, + dataRoutes, localManifest, mapRouteProperties, false, @@ -3619,23 +3722,20 @@ export function createRouter(init: RouterInit): Router { }); } catch (e) { return { type: "error", error: e, partialMatches }; - } finally { - // If we are not in the middle of an HMR revalidation and we changed the - // routes, provide a new identity so when we `updateState` at the end of - // this navigation/fetch `router.routes` will be a new identity and - // trigger a re-run of memoized `router.routes` dependencies. - // HMR will already update the identity and reflow when it lands - // `inFlightDataRoutes` in `completeNavigation` - if (isNonHMR && !signal.aborted) { - dataRoutes = [...dataRoutes]; - } } if (signal.aborted) { return { type: "aborted" }; } - let newMatches = matchRoutes(routesToUse, pathname, basename); + let activeBranches = dataRoutes.branches; + let newMatches = matchRoutesImpl( + dataRoutes.activeRoutes, + pathname, + basename, + false, + activeBranches, + ); let newPartialMatches: DataRouteMatch[] | null = null; if (newMatches) { @@ -3645,10 +3745,11 @@ export function createRouter(init: RouterInit): Router { } else { // Dynamic match - confirm this is the best match. newPartialMatches = matchRoutesImpl( - routesToUse, + dataRoutes.activeRoutes, pathname, basename, true, + activeBranches, ); // If we matched deeper into the same branch of `partialMatches` we were already @@ -3671,10 +3772,11 @@ export function createRouter(init: RouterInit): Router { // Perform partial matching if we didn't already do it above if (!newPartialMatches) { newPartialMatches = matchRoutesImpl( - routesToUse, + dataRoutes.activeRoutes, pathname, basename, true, + activeBranches, ); } @@ -3698,11 +3800,13 @@ export function createRouter(init: RouterInit): Router { function _internalSetRoutes(newRoutes: DataRouteObject[]) { manifest = {}; - inFlightDataRoutes = convertRoutesToDataRoutes( - newRoutes, - mapRouteProperties, - undefined, - manifest, + dataRoutes.setHmrRoutes( + convertRoutesToDataRoutes( + newRoutes, + mapRouteProperties, + undefined, + manifest, + ), ); } @@ -3711,12 +3815,10 @@ export function createRouter(init: RouterInit): Router { children: RouteObject[], unstable_allowElementMutations = false, ): void { - let isNonHMR = inFlightDataRoutes == null; - let routesToUse = inFlightDataRoutes || dataRoutes; patchRoutesImpl( routeId, children, - routesToUse, + dataRoutes, manifest, mapRouteProperties, unstable_allowElementMutations, @@ -3727,8 +3829,7 @@ export function createRouter(init: RouterInit): Router { // to re-run memoized `router.routes` dependencies. // HMR will already update the identity and reflow when it lands // `inFlightDataRoutes` in `completeNavigation` - if (isNonHMR) { - dataRoutes = [...dataRoutes]; + if (!dataRoutes.hasHMRRoutes) { updateState({}); } } @@ -3744,7 +3845,13 @@ export function createRouter(init: RouterInit): Router { return state; }, get routes() { - return dataRoutes; + return dataRoutes.stableRoutes; + }, + get branches() { + return dataRoutes.branches; + }, + get manifest() { + return manifest; }, get window() { return routerWindow; @@ -5057,6 +5164,7 @@ function getMatchesToLoad( routesToUse: DataRouteObject[], basename: string | undefined, hasPatchRoutesOnNavigation: boolean, + branches: RouteBranch[] | undefined, pendingActionResult?: PendingActionResult, callSiteDefaultShouldRevalidate?: boolean, ): { @@ -5216,7 +5324,13 @@ function getMatchesToLoad( let fetcher = state.fetchers.get(key); let isMidInitialLoad = fetcher && fetcher.state !== "idle" && fetcher.data === undefined; - let fetcherMatches = matchRoutes(routesToUse, f.path, basename); + let fetcherMatches = matchRoutesImpl( + routesToUse, + f.path, + basename ?? "/", + false, + branches, + ); // If the fetcher path no longer matches, push it in with null matches so // we can trigger a 404 in callLoadersAndMaybeResolveData. Note this is @@ -5435,7 +5549,7 @@ function shouldRevalidateLoader( function patchRoutesImpl( routeId: string | null, children: RouteObject[], - routesToUse: DataRouteObject[], + dataRoutes: DataRoutes, manifest: RouteManifest, mapRouteProperties: MapRoutePropertiesFunction, allowElementMutations: boolean, @@ -5452,7 +5566,7 @@ function patchRoutesImpl( } childrenToPatch = route.children; } else { - childrenToPatch = routesToUse; + childrenToPatch = dataRoutes.activeRoutes; } // Don't patch in routes we already know about so that `patch` is idempotent @@ -5512,6 +5626,14 @@ function patchRoutesImpl( }); } } + + // If we are not in the middle of an HMR revalidation, provide a new identity + // so when we `updateState` at the end of this navigation/fetch `router.routes` + // will be a new identity and trigger a re-run of memoized `router.routes` + // dependencies. + if (!dataRoutes.hasHMRRoutes) { + dataRoutes.setRoutes([...dataRoutes.activeRoutes]); + } } function isSameRoute( diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts index 73de96e58d..e00418b558 100644 --- a/packages/react-router/lib/server-runtime/server.ts +++ b/packages/react-router/lib/server-runtime/server.ts @@ -559,6 +559,7 @@ async function handleDocumentRequest( }; let entryContext: EntryContext = { manifest: build.assets, + branches: staticHandler._internalRouteBranches, routeModules: createEntryRouteModules(build.routes), staticHandlerContext: context, criticalCss, From 51a79a40b76f037caba448068fea90c5378e1897 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 4 May 2026 16:49:21 +0000 Subject: [PATCH 12/23] chore: generate markdown docs from jsdocs --- docs/api/data-routers/createStaticRouter.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api/data-routers/createStaticRouter.md b/docs/api/data-routers/createStaticRouter.md index b4f31d5571..7a5106b144 100644 --- a/docs/api/data-routers/createStaticRouter.md +++ b/docs/api/data-routers/createStaticRouter.md @@ -48,6 +48,7 @@ function createStaticRouter( routes: RouteObject[], context: StaticHandlerContext, opts: { + branches?: RouteBranch[]; future?: Partial; } = , ): DataRouter {} @@ -67,6 +68,10 @@ The [`StaticHandlerContext`](https://api.reactrouter.com/v7/interfaces/react-rou Future flags for the static [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) +### opts.branches + +Optional pre-computed route branches + ## Returns A static [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) that can be used to render the provided routes From 49295b5c220b567cff9934ddf0199877663b552c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 4 May 2026 14:17:38 -0400 Subject: [PATCH 13/23] Stabilize APIs (#14999) --- decisions/0015-observability.md | 2 +- docs/api/components/Form.md | 2 +- docs/api/components/Link.md | 8 +- docs/api/data-routers/RouterProvider.md | 6 +- docs/api/data-routers/createBrowserRouter.md | 4 +- docs/api/data-routers/createHashRouter.md | 4 +- docs/api/data-routers/createMemoryRouter.md | 4 +- docs/api/declarative-routers/BrowserRouter.md | 4 +- docs/api/declarative-routers/HashRouter.md | 4 +- docs/api/declarative-routers/HistoryRouter.md | 4 +- docs/api/declarative-routers/MemoryRouter.md | 4 +- docs/api/declarative-routers/Router.md | 4 +- .../react-router.config.ts.md | 10 +- docs/api/framework-routers/HydratedRouter.md | 2 +- docs/api/hooks/useLinkClickHandler.md | 18 +-- docs/explanation/react-transitions.md | 27 ++-- docs/how-to/error-reporting.md | 4 +- docs/how-to/instrumentation.md | 73 +++++----- docs/how-to/pre-rendering.md | 10 +- docs/how-to/react-server-components.md | 2 +- docs/upgrading/future.md | 24 ++-- examples/modal-data-router/src/App.tsx | 2 +- integration/browser-entry-test.ts | 2 +- integration/passthrough-requests-test.ts | 34 ++--- integration/rsc/rsc-prerender-test.ts | 2 +- integration/single-fetch-test.ts | 10 +- integration/sri-test.ts | 2 +- integration/vite-prerender-test.ts | 2 +- integration/vite-presets-test.ts | 4 +- .../minor.stabilize-pass-through-requests.md | 1 + .../minor.stabilize-prerender-concurrency.md | 1 + .../.changes/minor.stabilize-sri.md | 1 + packages/react-router-dev/config/config.ts | 73 +++++++--- packages/react-router-dev/vite/plugin.ts | 10 +- packages/react-router-dev/vite/rsc/plugin.ts | 8 +- ...nor.stabilize-default-should-revalidate.md | 1 + .../minor.stabilize-instrumentations.md | 3 + .../.changes/minor.stabilize-mask.md | 1 + .../minor.stabilize-normalize-path.md | 1 + .../minor.stabilize-pass-through-requests.md | 1 + .../.changes/minor.stabilize-sri.md | 1 + .../.changes/minor.stabilize-url.md | 1 + .../minor.stabilize-use-transitions.md | 1 + .../patch.made-mask-optional-in-location.md | 1 + ....made-unstablemask-optional-in-location.md | 1 - .../__tests__/dom/client-on-error-test.tsx | 32 ++--- .../dom/data-browser-router-test.tsx | 16 +-- .../__tests__/react-transitions-test.tsx | 32 ++--- .../__tests__/router/fetchers-test.ts | 44 +++--- .../__tests__/router/instrumentation-test.ts | 90 ++++++------ .../__tests__/router/mask-test.ts | 32 ++--- .../__tests__/router/router-test.ts | 28 ++-- .../router/should-revalidate-test.ts | 14 +- .../react-router/__tests__/router/ssr-test.ts | 20 +-- .../__tests__/router/submission-test.ts | 24 ++-- .../__tests__/server-runtime/utils.ts | 7 +- .../react-router/__tests__/utils/framework.ts | 1 - packages/react-router/index.ts | 12 +- packages/react-router/lib/components.tsx | 74 +++++----- packages/react-router/lib/context.ts | 10 +- .../lib/dom-export/hydrated-router.tsx | 26 ++-- packages/react-router/lib/dom/dom.ts | 2 +- packages/react-router/lib/dom/lib.tsx | 136 +++++++++--------- packages/react-router/lib/dom/server.tsx | 8 +- packages/react-router/lib/dom/ssr/entry.ts | 3 +- .../lib/dom/ssr/routes-test-stub.tsx | 6 +- packages/react-router/lib/dom/ssr/routes.tsx | 16 +-- packages/react-router/lib/hooks.tsx | 4 +- packages/react-router/lib/router/history.ts | 18 +-- .../lib/router/instrumentation.ts | 36 ++--- packages/react-router/lib/router/router.ts | 111 +++++++------- packages/react-router/lib/router/utils.ts | 18 +-- packages/react-router/lib/rsc/browser.tsx | 3 +- packages/react-router/lib/rsc/server.rsc.ts | 4 +- packages/react-router/lib/rsc/server.ssr.tsx | 3 +- .../react-router/lib/server-runtime/build.ts | 4 +- .../react-router/lib/server-runtime/data.ts | 8 +- .../react-router/lib/server-runtime/server.ts | 14 +- .../lib/server-runtime/single-fetch.ts | 8 +- packages/react-router/lib/types/route-data.ts | 32 ++--- .../framework-vite-6/react-router.config.ts | 4 +- .../react-router.config.ts | 2 +- playground/framework/react-router.config.ts | 2 +- pnpm-lock.yaml | 3 + scripts/bench/passthrough-requests.bench.mjs | 17 +-- scripts/package.json | 1 + 86 files changed, 667 insertions(+), 642 deletions(-) create mode 100644 packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md create mode 100644 packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md create mode 100644 packages/react-router-dev/.changes/minor.stabilize-sri.md create mode 100644 packages/react-router/.changes/minor.stabilize-default-should-revalidate.md create mode 100644 packages/react-router/.changes/minor.stabilize-instrumentations.md create mode 100644 packages/react-router/.changes/minor.stabilize-mask.md create mode 100644 packages/react-router/.changes/minor.stabilize-normalize-path.md create mode 100644 packages/react-router/.changes/minor.stabilize-pass-through-requests.md create mode 100644 packages/react-router/.changes/minor.stabilize-sri.md create mode 100644 packages/react-router/.changes/minor.stabilize-url.md create mode 100644 packages/react-router/.changes/minor.stabilize-use-transitions.md create mode 100644 packages/react-router/.changes/patch.made-mask-optional-in-location.md delete mode 100644 packages/react-router/.changes/patch.made-unstablemask-optional-in-location.md diff --git a/decisions/0015-observability.md b/decisions/0015-observability.md index d68ade8195..b81c0e0766 100644 --- a/decisions/0015-observability.md +++ b/decisions/0015-observability.md @@ -234,7 +234,7 @@ let unInstrumentedHandler = createRequestHandler({ ...m.entry, module: { ...m.entry.module, - unstable_instrumentations: undefined, + instrumentations: undefined, }, }, })), diff --git a/docs/api/components/Form.md b/docs/api/components/Form.md index aa2abc1a75..4cafa5dba6 100644 --- a/docs/api/components/Form.md +++ b/docs/api/components/Form.md @@ -144,7 +144,7 @@ Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/Vie for this navigation. To apply specific styles during the transition, see [`useViewTransitionState`](../hooks/useViewTransitionState). -### unstable_defaultShouldRevalidate +### defaultShouldRevalidate Specify the default revalidation behavior after this submission diff --git a/docs/api/components/Link.md b/docs/api/components/Link.md index cd92fa643c..b5fb8517be 100644 --- a/docs/api/components/Link.md +++ b/docs/api/components/Link.md @@ -214,14 +214,14 @@ for this navigation. To apply specific styles for the transition, see [`useViewTransitionState`](../hooks/useViewTransitionState) -### unstable_defaultShouldRevalidate +### defaultShouldRevalidate [modes: framework, data, declarative] Specify the default revalidation behavior for the navigation. ```tsx - + ``` If no `shouldRevalidate` functions are present on the active routes, then this @@ -232,7 +232,7 @@ useful when updating search params and you don't want to trigger a revalidation. By default (when not specified), loaders will revalidate according to the routers standard revalidation behavior. -### unstable_mask +### mask [modes: framework, data] @@ -265,7 +265,7 @@ export default function Gallery({ loaderData }: Route.ComponentProps) { {image.alt} diff --git a/docs/api/data-routers/RouterProvider.md b/docs/api/data-routers/RouterProvider.md index a14fccacbb..57aa0bcbee 100644 --- a/docs/api/data-routers/RouterProvider.md +++ b/docs/api/data-routers/RouterProvider.md @@ -49,7 +49,7 @@ function RouterProvider({ router, flushSync: reactDomFlushSyncImpl, onError, - unstable_useTransitions, + useTransitions, }: RouterProviderProps): React.ReactElement ``` @@ -78,7 +78,7 @@ and is only present for render errors. ```tsx { - let { location, params, unstable_pattern, errorInfo } = info; + let { location, params, pattern, errorInfo } = info; console.error(error, location, errorInfo); reportToErrorService(error, location, errorInfo); }} /> @@ -88,7 +88,7 @@ and is only present for render errors. The [`DataRouter`](https://api.reactrouter.com/v7/interfaces/react-router.DataRouter.html) instance to use for navigation and data fetching. -### unstable_useTransitions +### useTransitions Control whether router state updates are internally wrapped in [`React.startTransition`](https://react.dev/reference/react/startTransition). diff --git a/docs/api/data-routers/createBrowserRouter.md b/docs/api/data-routers/createBrowserRouter.md index d0abd36ff2..ed47ad12b2 100644 --- a/docs/api/data-routers/createBrowserRouter.md +++ b/docs/api/data-routers/createBrowserRouter.md @@ -172,7 +172,7 @@ const router = createBrowserRouter( ); ``` -### opts.unstable_instrumentations +### opts.instrumentations Array of instrumentation objects allowing you to instrument the router and individual routes prior to router initialization (and on any subsequently @@ -183,7 +183,7 @@ tracing. See the [docs](../../how-to/instrumentation) for more information. ```tsx let router = createBrowserRouter(routes, { - unstable_instrumentations: [logging] + instrumentations: [logging] }); diff --git a/docs/api/data-routers/createHashRouter.md b/docs/api/data-routers/createHashRouter.md index 90dc930027..16fa9e5b23 100644 --- a/docs/api/data-routers/createHashRouter.md +++ b/docs/api/data-routers/createHashRouter.md @@ -142,7 +142,7 @@ const router = createBrowserRouter( ); ``` -### opts.unstable_instrumentations +### opts.instrumentations Array of instrumentation objects allowing you to instrument the router and individual routes prior to router initialization (and on any subsequently @@ -153,7 +153,7 @@ tracing. See the [docs](../../how-to/instrumentation) for more information. ```tsx let router = createBrowserRouter(routes, { - unstable_instrumentations: [logging] + instrumentations: [logging] }); diff --git a/docs/api/data-routers/createMemoryRouter.md b/docs/api/data-routers/createMemoryRouter.md index 0715f908a4..25c25b63da 100644 --- a/docs/api/data-routers/createMemoryRouter.md +++ b/docs/api/data-routers/createMemoryRouter.md @@ -99,7 +99,7 @@ Initial entries in the in-memory history stack Index of `initialEntries` the application should initialize to -### opts.unstable_instrumentations +### opts.instrumentations Array of instrumentation objects allowing you to instrument the router and individual routes prior to router initialization (and on any subsequently @@ -110,7 +110,7 @@ tracing. See the [docs](../../how-to/instrumentation) for more information. ```tsx let router = createBrowserRouter(routes, { - unstable_instrumentations: [logging] + instrumentations: [logging] }); diff --git a/docs/api/declarative-routers/BrowserRouter.md b/docs/api/declarative-routers/BrowserRouter.md index 75ce9c9299..a0ad59eff9 100644 --- a/docs/api/declarative-routers/BrowserRouter.md +++ b/docs/api/declarative-routers/BrowserRouter.md @@ -31,7 +31,7 @@ API for client-side routing. function BrowserRouter({ basename, children, - unstable_useTransitions, + useTransitions, window, }: BrowserRouterProps) ``` @@ -46,7 +46,7 @@ Application basename ```` components describing your route configuration -### unstable_useTransitions +### useTransitions Control whether router state updates are internally wrapped in [`React.startTransition`](https://react.dev/reference/react/startTransition). diff --git a/docs/api/declarative-routers/HashRouter.md b/docs/api/declarative-routers/HashRouter.md index 658e4011bc..b67677ed7d 100644 --- a/docs/api/declarative-routers/HashRouter.md +++ b/docs/api/declarative-routers/HashRouter.md @@ -32,7 +32,7 @@ of the URL so it is not sent to the server. function HashRouter({ basename, children, - unstable_useTransitions, + useTransitions, window, }: HashRouterProps) ``` @@ -47,7 +47,7 @@ Application basename ```` components describing your route configuration -### unstable_useTransitions +### useTransitions Control whether router state updates are internally wrapped in [`React.startTransition`](https://react.dev/reference/react/startTransition). diff --git a/docs/api/declarative-routers/HistoryRouter.md b/docs/api/declarative-routers/HistoryRouter.md index 669b68cab1..522d950062 100644 --- a/docs/api/declarative-routers/HistoryRouter.md +++ b/docs/api/declarative-routers/HistoryRouter.md @@ -43,7 +43,7 @@ function HistoryRouter({ basename, children, history, - unstable_useTransitions, + useTransitions, }: HistoryRouterProps) ``` @@ -61,7 +61,7 @@ Application basename A `History` implementation for use by the router -### unstable_useTransitions +### useTransitions Control whether router state updates are internally wrapped in [`React.startTransition`](https://react.dev/reference/react/startTransition). diff --git a/docs/api/declarative-routers/MemoryRouter.md b/docs/api/declarative-routers/MemoryRouter.md index 7fa8c4286e..c04ccc39b2 100644 --- a/docs/api/declarative-routers/MemoryRouter.md +++ b/docs/api/declarative-routers/MemoryRouter.md @@ -32,7 +32,7 @@ function MemoryRouter({ children, initialEntries, initialIndex, - unstable_useTransitions, + useTransitions, }: MemoryRouterProps): React.ReactElement ``` @@ -54,7 +54,7 @@ Initial entries in the in-memory history stack Index of `initialEntries` the application should initialize to -### unstable_useTransitions +### useTransitions Control whether router state updates are internally wrapped in [`React.startTransition`](https://react.dev/reference/react/startTransition). diff --git a/docs/api/declarative-routers/Router.md b/docs/api/declarative-routers/Router.md index 8deecb412c..f5a93d50a6 100644 --- a/docs/api/declarative-routers/Router.md +++ b/docs/api/declarative-routers/Router.md @@ -38,7 +38,7 @@ function Router({ navigationType = NavigationType.Pop, navigator, static: staticProp = false, - unstable_useTransitions, + useTransitions, }: RouterProps): React.ReactElement | null ``` @@ -72,7 +72,7 @@ or a custom navigator that implements the [`Navigator`](https://api.reactrouter. Whether this router is static or not (used for SSR). If `true`, the router will not be reactive to location changes. -### unstable_useTransitions +### useTransitions Control whether router state updates are internally wrapped in [`React.startTransition`](https://react.dev/reference/react/startTransition). diff --git a/docs/api/framework-conventions/react-router.config.ts.md b/docs/api/framework-conventions/react-router.config.ts.md index 8e2b3f9395..58b601ab63 100644 --- a/docs/api/framework-conventions/react-router.config.ts.md +++ b/docs/api/framework-conventions/react-router.config.ts.md @@ -132,9 +132,7 @@ export default { ### `prerender` -An array of URLs to prerender to HTML files at build time. Can also be a function returning an array to dynamically generate URLs. - -See [Pre-Rendering][pre-rendering] for more information. +An array of URLs to prerender to HTML files at build time - see [Pre-Rendering][pre-rendering] for more information. ```tsx filename=react-router.config.ts export default { @@ -146,6 +144,12 @@ export default { const paths = await getStaticPaths(); return ["/", ...paths]; }, + + // Or an object if you wish to enable concurrency + prerender: { + paths: ["/", "/about", "/contact"], + concurrency: 4, + } } satisfies Config; ``` diff --git a/docs/api/framework-routers/HydratedRouter.md b/docs/api/framework-routers/HydratedRouter.md index 3b8c831d93..20ec986006 100644 --- a/docs/api/framework-routers/HydratedRouter.md +++ b/docs/api/framework-routers/HydratedRouter.md @@ -52,7 +52,7 @@ and is only present for render errors. ```tsx { - let { location, params, unstable_pattern, errorInfo } = info; + let { location, params, pattern, errorInfo } = info; console.error(error, location, errorInfo); reportToErrorService(error, location, errorInfo); }} /> diff --git a/docs/api/hooks/useLinkClickHandler.md b/docs/api/hooks/useLinkClickHandler.md index 250ea1001e..88740c66ae 100644 --- a/docs/api/hooks/useLinkClickHandler.md +++ b/docs/api/hooks/useLinkClickHandler.md @@ -34,23 +34,23 @@ function useLinkClickHandler( { target, replace: replaceProp, - unstable_mask, + mask, state, preventScrollReset, relative, viewTransition, - unstable_defaultShouldRevalidate, - unstable_useTransitions, + defaultShouldRevalidate, + useTransitions, }: { target?: React.HTMLAttributeAnchorTarget; replace?: boolean; - unstable_mask?: To; + mask?: To; state?: any; preventScrollReset?: boolean; relative?: RelativeRoutingType; viewTransition?: boolean; - unstable_defaultShouldRevalidate?: boolean; - unstable_useTransitions?: boolean; + defaultShouldRevalidate?: boolean; + useTransitions?: boolean; } = , ): (event: React.MouseEvent) => void {} ``` @@ -87,15 +87,15 @@ The target attribute for the link. Defaults to `undefined`. Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for this navigation. To apply specific styles during the transition, see [`useViewTransitionState`](../hooks/useViewTransitionState). Defaults to `false`. -### options.unstable_defaultShouldRevalidate +### options.defaultShouldRevalidate Specify the default revalidation behavior for the navigation. Defaults to `true`. -### options.unstable_mask +### options.mask Masked location to display in the browser instead of the router location. Defaults to `undefined`. -### options.unstable_useTransitions +### options.useTransitions Wraps the navigation in [`React.startTransition`](https://react.dev/reference/react/startTransition) for concurrent rendering. Defaults to `false`. diff --git a/docs/explanation/react-transitions.md b/docs/explanation/react-transitions.md index 6b04049468..a17af5597a 100644 --- a/docs/explanation/react-transitions.md +++ b/docs/explanation/react-transitions.md @@ -1,6 +1,5 @@ --- title: React Transitions -unstable: true --- # React Transitions @@ -10,10 +9,6 @@ unstable: true

-The `unstable_useTransitions` prop is experimental and subject to breaking changes in -minor/patch releases. Please use with caution and pay **very** close attention -to release notes for relevant changes. - [React 18][react-18] introduced the concept of "transitions" which allow you to differentiate urgent from non-urgent UI updates. To learn more about React Transitions and "concurrent rendering" Please refer to React's official documentation: - [What is Concurrent React][concurrent] @@ -27,13 +22,13 @@ to release notes for relevant changes. The introduction of Transitions in React makes the story of how React Router manages your navigations and router state a bit more complicated. These are powerful APIs but they don't come without some nuance and added complexity. We aim to make React Router work seamlessly with the new React features, but in some cases there may exist some tension between the new React ways to do things and some patterns you are already using in your React Router apps (i.e., pending states, optimistic UI). -To ensure a smooth adoption story, we've introduced changes related to Transitions behind an opt-in `unstable_useTransitions` flag so that you can upgrade in a non-breaking fashion. +To ensure a smooth adoption story, we've introduced changes related to Transitions behind an opt-in `useTransitions` flag so that you can upgrade in a non-breaking fashion. ### Current Behavior We first leveraged `React.startTransition` to make React Router more Suspense-friendly in React Router [6.13.0][rr-6-13-0] via the `future.v7_startTransition` flag. In v7, that became the default behavior and all router state updates are currently wrapped in `React.startTransition`. -This default behavior has 2 potential issues that `unstable_useTransitions` is designed to solve: +This default behavior has 2 potential issues that `useTransitions` is designed to solve: - There are some valid use cases where you _don't_ want your updates wrapped in `startTransition` - One specific issue is that `React.useSyncExternalStore` updates can't be Transitions ([^1][uses-transition-issue], [^2][uses-transition-tweet]). `useSyncExternalStore` forces a sync update, which means fallbacks can be shown in update transitions that would otherwise avoid showing the fallback. @@ -41,26 +36,26 @@ This default behavior has 2 potential issues that `unstable_useTransitions` is d - React 19 has added a new `startTransition(() => Promise))` API as well as a new `useOptimistic` hook to surface updates during Transitions - Without some updates to React Router, `startTransition(() => navigate(path))` doesn't work as you might expect, because we are not using `useOptimistic` internally so router state updates don't surface during the navigation, which breaks hooks like `useNavigation` -To provide a solution to both of the above issues, we're introducing a new `unstable_useTransitions` prop to the router components that will let you opt-out of using `startTransition` for router state updates (solving the first issue), or opt-into a more enhanced usage of `startTransition` + `useOptimistic` (solving the second issue). Because the current behavior is a bit incomplete with the new React 19 APIs, we plan to make the opt-in behavior the default in React Router v8, but we will likely retain the opt-out flag for use cases such as `useSyncExternalStore`. +To provide a solution to both of the above issues, we're introducing a new `useTransitions` prop to the router components that will let you opt-out of using `startTransition` for router state updates (solving the first issue), or opt-into a more enhanced usage of `startTransition` + `useOptimistic` (solving the second issue). Because the current behavior is a bit incomplete with the new React 19 APIs, we plan to make the opt-in behavior the default in React Router v8, but we will likely retain the opt-out flag for use cases such as `useSyncExternalStore`. -### Opt-out via `unstable_useTransitions=false` +### Opt-out via `useTransitions=false` If your application is not "Transition-friendly" due to the usage of `useSyncExternalStore` (or other reasons), then you can opt-out via the prop: ```tsx // Framework Mode (entry.client.tsx) - + // Data Mode - + // Declarative Mode - + ``` This will stop the router from wrapping internal state updates in `startTransition`. -### Opt-in via `unstable_useTransitions=true` +### Opt-in via `useTransitions=true` Opting into this feature in Framework or Data Mode requires that you are using React 19 because it needs access to [`React.useOptimistic`][use-optimistic] @@ -68,13 +63,13 @@ If you want to make your application play nicely with all of the new React 19 fe ```tsx // Framework Mode (entry.client.tsx) - + // Data Mode - + // Declarative Mode - + ``` With this flag enabled: diff --git a/docs/how-to/error-reporting.md b/docs/how-to/error-reporting.md index 72539d60da..c111d659b6 100644 --- a/docs/how-to/error-reporting.md +++ b/docs/how-to/error-reporting.md @@ -75,7 +75,7 @@ import { type ClientOnErrorFunction } from "react-router"; const onError: ClientOnErrorFunction = ( error, - { location, params, unstable_pattern, errorInfo }, + { location, params, pattern, errorInfo }, ) => { myReportError(error, location, errorInfo); @@ -108,7 +108,7 @@ import { type ClientOnErrorFunction } from "react-router"; const onError: ClientOnErrorFunction = ( error, - { location, params, unstable_pattern, errorInfo }, + { location, params, pattern, errorInfo }, ) => { myReportError(error, location, errorInfo); diff --git a/docs/how-to/instrumentation.md b/docs/how-to/instrumentation.md index 8bb39ef6fb..adf5defc49 100644 --- a/docs/how-to/instrumentation.md +++ b/docs/how-to/instrumentation.md @@ -1,6 +1,5 @@ --- title: Instrumentation -unstable: true --- # Instrumentation @@ -10,10 +9,6 @@ unstable: true

-The instrumentation APIs are experimental and subject to breaking changes in -minor/patch releases. Please use with caution and pay **very** close attention -to release notes for relevant changes. - Instrumentation allows you to add logging, error reporting, and performance tracing to your React Router application without modifying your actual route handlers. This enables comprehensive observability solutions for production applications on both the server and client. ## Overview @@ -41,7 +36,7 @@ As with any instrumentation approach, adding additional code execution at runtim Add instrumentations to your `entry.server.tsx`: ```tsx filename=app/entry.server.tsx -export const unstable_instrumentations = [ +export const instrumentations = [ { // Instrument the server handler handler(handler) { @@ -90,7 +85,7 @@ import { startTransition, StrictMode } from "react"; import { hydrateRoot } from "react-dom/client"; import { HydratedRouter } from "react-router/dom"; -const unstable_instrumentations = [ +const instrumentations = [ { // Instrument router operations router(router) { @@ -141,8 +136,8 @@ startTransition(() => { document, , @@ -162,7 +157,7 @@ import { RouterProvider, } from "react-router"; -const unstable_instrumentations = [ +const instrumentations = [ { // Instrument router operations router(router) { @@ -209,7 +204,7 @@ const unstable_instrumentations = [ ]; const router = createBrowserRouter(routes, { - unstable_instrumentations, + instrumentations, }); function App() { @@ -230,7 +225,7 @@ There are different levels at which you can instrument your application. Each in Instruments the top-level request handler that processes all requests to your server: ```tsx filename=entry.server.tsx -export const unstable_instrumentations = [ +export const instrumentations = [ { handler(handler) { handler.instrument({ @@ -251,7 +246,7 @@ export const unstable_instrumentations = [ Instruments client-side router operations like navigations and fetcher calls: ```tsx -export const unstable_instrumentations = [ +export const instrumentations = [ { router(router) { router.instrument({ @@ -273,12 +268,12 @@ export const unstable_instrumentations = [ // Framework Mode (entry.client.tsx) ; // Data Mode const router = createBrowserRouter(routes, { - unstable_instrumentations, + instrumentations, }); ``` @@ -289,27 +284,27 @@ const router = createBrowserRouter(routes, { Instruments individual route handlers: ```tsx -const unstable_instrumentations = [ +const instrumentations = [ { route(route) { route.instrument({ async loader( callLoader, - { params, request, context, unstable_pattern }, + { params, request, context, pattern }, ) { // Runs around loader execution await callLoader(); }, async action( callAction, - { params, request, context, unstable_pattern }, + { params, request, context, pattern }, ) { // Runs around action execution await callAction(); }, async middleware( callMiddleware, - { params, request, context, unstable_pattern }, + { params, request, context, pattern }, ) { // Runs around middleware execution await callMiddleware(); @@ -341,7 +336,7 @@ To ensure that instrumentation code doesn't impact the runtime application, erro First, if a "handler" function (loader, action, request handler, navigation, etc.) throws an error, that error will not bubble out of the `callHandler` function invoked from your instrumentation. Instead, the `callHandler` function returns a discriminated union result of type `{ type: "success", error: undefined } | { type: "error", error: unknown }`. This ensures your entire instrumentation function runs without needing any try/catch/finally logic to handle application errors. ```tsx -export const unstable_instrumentations = [ +export const instrumentations = [ { route(route) { route.instrument({ @@ -363,7 +358,7 @@ export const unstable_instrumentations = [ Second, if your instrumentation function throws an error, React Router will gracefully swallow that so that it does not bubble outward and impact other instrumentations or application behavior. In both of these examples, the handlers and all other instrumentation functions will still run: ```tsx -export const unstable_instrumentations = [ +export const instrumentations = [ { route(route) { route.instrument({ @@ -390,7 +385,7 @@ export const unstable_instrumentations = [ You can compose multiple instrumentations by providing an array: ```tsx -export const unstable_instrumentations = [ +export const instrumentations = [ loggingInstrumentation, performanceInstrumentation, errorReportingInstrumentation, @@ -404,7 +399,7 @@ Each instrumentation wraps the previous one, creating a nested execution chain. You can enable instrumentation conditionally based on environment or other factors: ```tsx -export const unstable_instrumentations = +export const instrumentations = process.env.NODE_ENV === "production" ? [productionInstrumentation] : [developmentInstrumentation]; @@ -412,7 +407,7 @@ export const unstable_instrumentations = ```tsx // Or conditionally within an instrumentation -export const unstable_instrumentations = [ +export const instrumentations = [ { route(route) { // Only instrument specific routes @@ -437,7 +432,7 @@ export const unstable_instrumentations = [ ### Request logging (server) ```tsx -const logging: unstable_ServerInstrumentation = { +const logging: ServerInstrumentation = { handler({ instrument }) { instrument({ request: (fn, { request }) => @@ -455,7 +450,7 @@ const logging: unstable_ServerInstrumentation = { async function log( label: string, - cb: () => Promise, + cb: () => Promise, ) { let start = Date.now(); console.log(`➡️ ${label}`); @@ -463,7 +458,7 @@ async function log( console.log(`⬅️ ${label} (${Date.now() - start}ms)`); } -export const unstable_instrumentations = [logging]; +export const instrumentations = [logging]; ``` ### OpenTelemetry Integration @@ -473,7 +468,7 @@ import { trace, SpanStatusCode } from "@opentelemetry/api"; const tracer = trace.getTracer("my-app"); -const otel: unstable_ServerInstrumentation = { +const otel: ServerInstrumentation = { handler({ instrument }) { instrument({ request: (fn, { request }) => @@ -482,22 +477,22 @@ const otel: unstable_ServerInstrumentation = { }, route({ instrument, id }) { instrument({ - middleware: (fn, { unstable_pattern }) => + middleware: (fn, { pattern }) => otelSpan( "middleware", - { routeId: id, pattern: unstable_pattern }, + { routeId: id, pattern: pattern }, fn, ), - loader: (fn, { unstable_pattern }) => + loader: (fn, { pattern }) => otelSpan( "loader", - { routeId: id, pattern: unstable_pattern }, + { routeId: id, pattern: pattern }, fn, ), - action: (fn, { unstable_pattern }) => + action: (fn, { pattern }) => otelSpan( "action", - { routeId: id, pattern: unstable_pattern }, + { routeId: id, pattern: pattern }, fn, ), }); @@ -507,7 +502,7 @@ const otel: unstable_ServerInstrumentation = { async function otelSpan( label: string, attributes: Record, - cb: () => Promise, + cb: () => Promise, ) { return tracer.startActiveSpan( label, @@ -525,13 +520,13 @@ async function otelSpan( ); } -export const unstable_instrumentations = [otel]; +export const instrumentations = [otel]; ``` ### Client-side Performance Tracking ```tsx -const windowPerf: unstable_ClientInstrumentation = { +const windowPerf: ClientInstrumentation = { router({ instrument }) { instrument({ navigate: (fn, { to, currentUrl }) => @@ -551,7 +546,7 @@ const windowPerf: unstable_ClientInstrumentation = { async function measure( label: string, - cb: () => Promise, + cb: () => Promise, ) { performance.mark(`start:${label}`); await cb(); @@ -563,5 +558,5 @@ async function measure( ); } -; +; ``` diff --git a/docs/how-to/pre-rendering.md b/docs/how-to/pre-rendering.md index e0cbc6a85d..07db5f70a4 100644 --- a/docs/how-to/pre-rendering.md +++ b/docs/how-to/pre-rendering.md @@ -59,15 +59,11 @@ export default { } satisfies Config; ``` -### Concurrency (unstable) - -This API is experimental and subject to breaking changes in -minor/patch releases. Please use with caution and pay **very** close attention -to release notes for relevant changes. +### Concurrency By default, pages are pre-rendered one path at a time. You can enable concurrency to pre-render multiple paths in parallel which can speed up build times in many cases. You should experiment with the value that provides the best performance for your app. -To specify concurrency, move your `prerender` config down into a `prerender.paths` field and you can specify the concurrency in `prerender.unstable_concurrency`: +To specify concurrency, move your `prerender` config down into a `prerender.paths` field and you can specify the concurrency in `prerender.concurrency`: ```ts filename=react-router.config.ts import type { Config } from "@react-router/dev/config"; @@ -81,7 +77,7 @@ export default { "/blog", ...slugs.map((s) => `/blog/${s}`), ], - unstable_concurrency: 4, + concurrency: 4, }, } satisfies Config; ``` diff --git a/docs/how-to/react-server-components.md b/docs/how-to/react-server-components.md index 1773361613..f791a95918 100644 --- a/docs/how-to/react-server-components.md +++ b/docs/how-to/react-server-components.md @@ -385,7 +385,7 @@ The following options from `react-router.config.ts` are not currently supported - `presets` - `serverBundles` - `future.v8_splitRouteModules` -- `future.unstable_subResourceIntegrity` +- `subResourceIntegrity` ## RSC Data Mode diff --git a/docs/upgrading/future.md b/docs/upgrading/future.md index 5eaeefcdab..4f2fb63c7b 100644 --- a/docs/upgrading/future.md +++ b/docs/upgrading/future.md @@ -119,11 +119,7 @@ export default { No code changes are required unless you have custom Vite configuration that needs to be updated for the [Environment API][vite-environment]. Most users won't need to make any changes. -## Unstable Future Flags (Optional) - -We document some [unstable] flags here as a reference for folks contributing to the project via beta testing, but they are not generally recommended for production use and may having breaking changes patch/minor releases - adopt with caution! - -### future.unstable_passThroughRequests +## `future.v8_passThroughRequests` [MODES: framework] @@ -137,9 +133,9 @@ By default, React Router normalizes the `request.url` passed to your `loader`, ` This flag eliminates that normalization and passes the raw HTTP `request` instance to your handlers. This provides a few benefits: - Reduces server-side overhead by eliminating multiple `new Request()` calls on the critical path -- Allows you to distinguish document from data requests in your handlers base don the presence of a `.data` suffix (useful for [observability] purposes) +- Allows you to distinguish document from data requests in your handlers based on the presence of a `.data` suffix (useful for [observability] purposes) -If you were previously relying on the normalization of `request.url`, you can switch to use the new sibling `unstable_url` parameter which contains a `URL` instance representing the normalized location. +If you were previously relying on the normalization of `request.url`, you can switch to use the new sibling `url` parameter which contains a `URL` instance representing the normalized location. 👉 **Enable the Flag** @@ -148,7 +144,7 @@ import type { Config } from "@react-router/dev/config"; export default { future: { - unstable_passThroughRequests: true, + v8_passThroughRequests: true, }, } satisfies Config; ``` @@ -169,13 +165,13 @@ export async function loader({ } } -// ✅ After: use `unstable_url` for normalized routing logic and `request.url` +// ✅ After: use `url` for normalized routing logic and `request.url` // for raw routing logic export async function loader({ request, - unstable_url, + url, }: Route.LoaderArgs) { - if (unstable_url.pathname === "/path") { + if (url.pathname === "/path") { // This will always have the `.data` suffix stripped } @@ -186,6 +182,12 @@ export async function loader({ } ``` +## Unstable Future Flags (Optional) + +We document some [unstable] flags here as a reference for folks contributing to the project via beta testing, but they are not generally recommended for production use and may having breaking changes patch/minor releases - adopt with caution! + +_No current unstable flags to document_ + [api-development-strategy]: ../community/api-development-strategy [unstable]: ../community/api-development-strategy#unstable-flags [observability]: ../how-to/instrumentation diff --git a/examples/modal-data-router/src/App.tsx b/examples/modal-data-router/src/App.tsx index 6d24db54b8..fbe6c3d382 100644 --- a/examples/modal-data-router/src/App.tsx +++ b/examples/modal-data-router/src/App.tsx @@ -159,7 +159,7 @@ function Gallery() { - + Go to new page

{loaderData.url}

@@ -77,11 +77,11 @@ const files = { } `, "app/routes/page.tsx": js` - export function loader({ request, unstable_url }) { - let url = new URL(request.url); + export function loader({ request, url }) { + let passthroughUrl = new URL(request.url); return { - url: url.pathname + url.search, - path: unstable_url.pathname + unstable_url.search + url: passthroughUrl.pathname + passthroughUrl.search, + path: url.pathname + url.search }; } @@ -97,14 +97,14 @@ const files = { }; test.describe("pass through requests", () => { - test("sends proper arguments to loaders when future.unstable_passThroughRequests is disabled", async ({ + test("sends proper arguments to loaders when future.v8_passThroughRequests is disabled", async ({ page, }) => { let fixture = await createFixture({ files: { "react-router.config.ts": reactRouterConfig({ future: { - unstable_passThroughRequests: false, + v8_passThroughRequests: false, }, }), ...files, @@ -160,14 +160,14 @@ test.describe("pass through requests", () => { requests = []; }); - test("sends proper arguments to loaders when future.unstable_passThroughRequests is enabled", async ({ + test("sends proper arguments to loaders when future.v8_passThroughRequests is enabled", async ({ page, }) => { let fixture = await createFixture({ files: { "react-router.config.ts": reactRouterConfig({ future: { - unstable_passThroughRequests: true, + v8_passThroughRequests: true, }, }), ...files, diff --git a/integration/rsc/rsc-prerender-test.ts b/integration/rsc/rsc-prerender-test.ts index acf3e4ab31..7507558cdf 100644 --- a/integration/rsc/rsc-prerender-test.ts +++ b/integration/rsc/rsc-prerender-test.ts @@ -37,7 +37,7 @@ function prerender({ | PrerenderPaths | { paths: PrerenderPaths; - unstable_concurrency?: number; + concurrency?: number; }; ssr?: boolean; vitePreview: ( diff --git a/integration/single-fetch-test.ts b/integration/single-fetch-test.ts index dbb23f4ec2..614ab84272 100644 --- a/integration/single-fetch-test.ts +++ b/integration/single-fetch-test.ts @@ -857,7 +857,7 @@ test.describe("single-fetch", () => { export default function Comp({ loaderData, actionData }) { return ( -
+

{loaderData.count}

{actionData ?

{actionData.count}

: null} @@ -911,7 +911,7 @@ test.describe("single-fetch", () => { export default function Comp({ loaderData }) { let navigation = useNavigation(); return ( - +

{loaderData.count}

{navigation.state === "idle" ?

idle

: null} @@ -970,7 +970,7 @@ test.describe("single-fetch", () => { export default function Comp({ loaderData, actionData }) { return ( - +

{loaderData.count}

{actionData ?

{actionData.count}

: null} @@ -1028,7 +1028,7 @@ test.describe("single-fetch", () => { export default function Comp({ loaderData }) { let navigation = useNavigation(); return ( - +

{loaderData.count}

{navigation.state === "idle" ?

idle

: null} @@ -1088,7 +1088,7 @@ test.describe("single-fetch", () => {
                     {JSON.stringify(useMatches().map(m => [m.id, m.data]))}
diff --git a/integration/sri-test.ts b/integration/sri-test.ts
index 1178994aff..3ccd26b153 100644
--- a/integration/sri-test.ts
+++ b/integration/sri-test.ts
@@ -19,7 +19,7 @@ test.describe("CSub-Resource Integrity", () => {
     fixture = await createFixture({
       files: {
         "react-router.config.ts": reactRouterConfig({
-          future: { unstable_subResourceIntegrity: true },
+          subResourceIntegrity: true,
         }),
         "app/root.tsx": js`
           import { Links, Meta, Outlet, Scripts } from "react-router";
diff --git a/integration/vite-prerender-test.ts b/integration/vite-prerender-test.ts
index 66f611cf05..5efe5f1a33 100644
--- a/integration/vite-prerender-test.ts
+++ b/integration/vite-prerender-test.ts
@@ -626,7 +626,7 @@ for (let previewServerPrerendering of [false, true]) {
             "react-router.config.ts": reactRouterConfig({
               prerender: {
                 paths: ["/", "/about"],
-                unstable_concurrency: 2,
+                concurrency: 2,
               },
             }),
             "vite.config.ts": js`
diff --git a/integration/vite-presets-test.ts b/integration/vite-presets-test.ts
index abf835389a..a15aa2bd68 100644
--- a/integration/vite-presets-test.ts
+++ b/integration/vite-presets-test.ts
@@ -238,6 +238,7 @@ test.describe("Vite / presets", async () => {
         "serverBundles",
         "serverModuleFormat",
         "ssr",
+        "subResourceIntegrity",
         "allowedActionOrigins",
         "unstable_routeConfig",
       ]);
@@ -245,11 +246,10 @@ test.describe("Vite / presets", async () => {
       // Ensure future flags from presets are properly merged
       expect(buildEndArgsMeta.futureFlags).toEqual({
         unstable_optimizeDeps: true,
-        unstable_passThroughRequests: false,
-        unstable_subResourceIntegrity: false,
         unstable_trailingSlashAwareDataRequests: false,
         unstable_previewServerPrerendering: false,
         v8_middleware: true,
+        v8_passThroughRequests: false,
         v8_splitRouteModules: false,
         v8_viteEnvironmentApi: false,
       });
diff --git a/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md b/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md
new file mode 100644
index 0000000000..fa624d1d0f
--- /dev/null
+++ b/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md
@@ -0,0 +1 @@
+Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests`
diff --git a/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md b/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md
new file mode 100644
index 0000000000..8ce1239d9d
--- /dev/null
+++ b/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md
@@ -0,0 +1 @@
+Stabilize `prerender.unstable_concurrency` as `prerender.concurrency`
diff --git a/packages/react-router-dev/.changes/minor.stabilize-sri.md b/packages/react-router-dev/.changes/minor.stabilize-sri.md
new file mode 100644
index 0000000000..aec5614143
--- /dev/null
+++ b/packages/react-router-dev/.changes/minor.stabilize-sri.md
@@ -0,0 +1 @@
+Stabilize `future.unstable_subResourceIntegrity` as a top-level `subResourceIntegrity` config option in `react-router.config.ts`
diff --git a/packages/react-router-dev/config/config.ts b/packages/react-router-dev/config/config.ts
index f611fd22cc..52d1ff1f1d 100644
--- a/packages/react-router-dev/config/config.ts
+++ b/packages/react-router-dev/config/config.ts
@@ -86,8 +86,7 @@ type ValidateConfigFunction = (config: ReactRouterConfig) => string | void;
 
 interface FutureConfig {
   unstable_optimizeDeps: boolean;
-  unstable_passThroughRequests: boolean;
-  unstable_subResourceIntegrity: boolean;
+  v8_passThroughRequests: boolean;
   unstable_trailingSlashAwareDataRequests: boolean;
   /**
    * Prerender with Vite Preview server
@@ -162,7 +161,7 @@ export type ReactRouterConfig = {
    * An array of URLs to prerender to HTML files at build time.  Can also be a
    * function returning an array to dynamically generate URLs.
    *
-   * `unstable_concurrency` defaults to 1, which means "no concurrency" - fully serial execution.
+   * `concurrency` defaults to 1, which means "no concurrency" - fully serial execution.
    * Setting it to a value more than 1 enables concurrent prerendering.
    * Setting it to a value higher than one can increase the speed of the build,
    * but may consume more resources, and send more concurrent requests to the
@@ -172,7 +171,7 @@ export type ReactRouterConfig = {
     | PrerenderPaths
     | {
         paths: PrerenderPaths;
-        unstable_concurrency?: number;
+        concurrency?: number;
       };
   /**
    * An array of React Router plugin config presets to ease integration with
@@ -217,6 +216,12 @@ export type ReactRouterConfig = {
    */
   ssr?: boolean;
 
+  /**
+   * Enable subresource integrity hashes on asset script tags. Defaults to
+   * `false`.
+   */
+  subResourceIntegrity?: boolean;
+
   /**
    * An array of allowed origin hosts for action submissions to UI routes (does not apply
    * to resource routes). Supports micromatch glob patterns (`*` to match one segment,
@@ -324,6 +329,10 @@ export type ResolvedReactRouterConfig = Readonly<{
    * SPA without server-rendering. Default's to `true`.
    */
   ssr: boolean;
+  /**
+   * Whether to generate subresource integrity hashes for asset script tags.
+   */
+  subResourceIntegrity: boolean;
   /**
    * The allowed origins for actions / mutations. Does not apply to routes
    * without a component. micromatch glob patterns are supported.
@@ -544,16 +553,22 @@ async function resolveConfig({
       );
     }
 
+    if (typeof prerender === "object" && "unstable_concurrency" in prerender) {
+      return err(
+        "The `prerender.unstable_concurrency` config field has been stabilized as `prerender.concurrency`",
+      );
+    }
+
     let isValidConcurrencyConfig =
       typeof prerender != "object" ||
-      !("unstable_concurrency" in prerender) ||
-      (typeof prerender.unstable_concurrency === "number" &&
-        Number.isInteger(prerender.unstable_concurrency) &&
-        prerender.unstable_concurrency > 0);
+      !("concurrency" in prerender) ||
+      (typeof prerender.concurrency === "number" &&
+        Number.isInteger(prerender.concurrency) &&
+        prerender.concurrency > 0);
 
     if (!isValidConcurrencyConfig) {
       return err(
-        "The `prerender.unstable_concurrency` config must be a positive integer if specified.",
+        "The `prerender.concurrency` config must be a positive integer if specified.",
       );
     }
   }
@@ -670,25 +685,35 @@ async function resolveConfig({
   }
 
   // Check for renamed flags and provide helpful error messages
-  let futureConfig = userAndPresetConfigs.future as any;
-  if (futureConfig?.unstable_splitRouteModules !== undefined) {
-    return err(
-      'The "future.unstable_splitRouteModules" flag has been stabilized as "future.v8_splitRouteModules"',
-    );
-  }
-  if (futureConfig?.unstable_viteEnvironmentApi !== undefined) {
-    return err(
-      'The "future.unstable_viteEnvironmentApi" flag has been stabilized as "future.v8_viteEnvironmentApi"',
-    );
+  let futureConfig = userAndPresetConfigs.future;
+  if (futureConfig) {
+    if ("unstable_splitRouteModules" in futureConfig) {
+      return err(
+        "The `future.unstable_splitRouteModules` flag has been stabilized as `future.v8_splitRouteModules`",
+      );
+    }
+    if ("unstable_viteEnvironmentApi" in futureConfig) {
+      return err(
+        "The `future.unstable_viteEnvironmentApi` flag has been stabilized as `future.v8_viteEnvironmentApi`",
+      );
+    }
+    if ("unstable_passThroughRequests" in futureConfig) {
+      return err(
+        "The `future.unstable_passThroughRequests` flag has been stabilized as `future.v8_passThroughRequests`",
+      );
+    }
+    if ("unstable_subResourceIntegrity" in futureConfig) {
+      return err(
+        "The `future.unstable_subResourceIntegrity` flag has been stabilized and moved to a top-level `config.subResourceIntegrity` field",
+      );
+    }
   }
 
   let future: FutureConfig = {
     unstable_optimizeDeps:
       userAndPresetConfigs.future?.unstable_optimizeDeps ?? false,
-    unstable_passThroughRequests:
-      userAndPresetConfigs.future?.unstable_passThroughRequests ?? false,
-    unstable_subResourceIntegrity:
-      userAndPresetConfigs.future?.unstable_subResourceIntegrity ?? false,
+    v8_passThroughRequests:
+      userAndPresetConfigs.future?.v8_passThroughRequests ?? false,
     unstable_trailingSlashAwareDataRequests:
       userAndPresetConfigs.future?.unstable_trailingSlashAwareDataRequests ??
       false,
@@ -704,6 +729,7 @@ async function resolveConfig({
   };
 
   let allowedActionOrigins = userAndPresetConfigs.allowedActionOrigins ?? false;
+  let subResourceIntegrity = userAndPresetConfigs.subResourceIntegrity ?? false;
 
   let reactRouterConfig: ResolvedReactRouterConfig = deepFreeze({
     appDirectory,
@@ -718,6 +744,7 @@ async function resolveConfig({
     serverBundles,
     serverModuleFormat,
     ssr,
+    subResourceIntegrity,
     allowedActionOrigins,
     unstable_routeConfig: routeConfig,
   } satisfies ResolvedReactRouterConfig);
diff --git a/packages/react-router-dev/vite/plugin.ts b/packages/react-router-dev/vite/plugin.ts
index 66db51ca51..60180d447d 100644
--- a/packages/react-router-dev/vite/plugin.ts
+++ b/packages/react-router-dev/vite/plugin.ts
@@ -1107,7 +1107,7 @@ export const reactRouterVitePlugin: ReactRouterVitePlugin = () => {
     );
 
     let sri: ReactRouterManifest["sri"] = undefined;
-    if (ctx.reactRouterConfig.future.unstable_subResourceIntegrity) {
+    if (ctx.reactRouterConfig.subResourceIntegrity) {
       sri = await generateSriManifest(ctx);
     }
 
@@ -3263,8 +3263,8 @@ async function handlePrerender(
 
   let concurrency = 1;
   let { prerender } = reactRouterConfig;
-  if (typeof prerender === "object" && "unstable_concurrency" in prerender) {
-    concurrency = prerender.unstable_concurrency ?? 1;
+  if (typeof prerender === "object" && "concurrency" in prerender) {
+    concurrency = prerender.concurrency ?? 1;
   }
 
   const pMap = await import("p-map");
@@ -4270,8 +4270,8 @@ function getPrerenderConcurrencyConfig(
 ): number {
   let concurrency = 1;
   let { prerender } = reactRouterConfig;
-  if (typeof prerender === "object" && "unstable_concurrency" in prerender) {
-    concurrency = prerender.unstable_concurrency ?? 1;
+  if (typeof prerender === "object" && "concurrency" in prerender) {
+    concurrency = prerender.concurrency ?? 1;
   }
   return concurrency;
 }
diff --git a/packages/react-router-dev/vite/rsc/plugin.ts b/packages/react-router-dev/vite/rsc/plugin.ts
index 14a15d3544..fa9878f1eb 100644
--- a/packages/react-router-dev/vite/rsc/plugin.ts
+++ b/packages/react-router-dev/vite/rsc/plugin.ts
@@ -166,8 +166,8 @@ export function reactRouterRSCVitePlugin(): Vite.PluginOption[] {
               errors.push("future.v8_middleware: false");
             if (userConfig.future?.v8_viteEnvironmentApi === false)
               errors.push("future.v8_viteEnvironmentApi: false");
-            if (userConfig.future?.unstable_subResourceIntegrity)
-              errors.push("future.unstable_subResourceIntegrity");
+            if (userConfig.subResourceIntegrity)
+              errors.push("subResourceIntegrity");
             if (errors.length) {
               return `RSC Framework Mode does not currently support the following React Router config:\n${errors.map((x) => ` - ${x}`).join("\n")}\n`;
             }
@@ -805,8 +805,8 @@ function getPrerenderConcurrencyConfig(
 ): number {
   let concurrency = 1;
   let { prerender } = reactRouterConfig;
-  if (typeof prerender === "object" && "unstable_concurrency" in prerender) {
-    concurrency = prerender.unstable_concurrency ?? 1;
+  if (typeof prerender === "object" && "concurrency" in prerender) {
+    concurrency = prerender.concurrency ?? 1;
   }
   return concurrency;
 }
diff --git a/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md b/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md
new file mode 100644
index 0000000000..4c44d53229
--- /dev/null
+++ b/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md
@@ -0,0 +1 @@
+Stabilize `unstable_defaultShouldRevalidate` as `defaultShouldRevalidate` on ``, ``, `useLinkClickHandler`, `useSubmit`, `fetcher.submit`, and `setSearchParams`
diff --git a/packages/react-router/.changes/minor.stabilize-instrumentations.md b/packages/react-router/.changes/minor.stabilize-instrumentations.md
new file mode 100644
index 0000000000..ece1fa949f
--- /dev/null
+++ b/packages/react-router/.changes/minor.stabilize-instrumentations.md
@@ -0,0 +1,3 @@
+Stabilize the instrumentation APIs. `unstable_instrumentations` is now `instrumentations` and `unstable_pattern` is now `pattern`
+
+- The `unstable_ServerInstrumentation`, `unstable_ClientInstrumentation`, `unstable_InstrumentRequestHandlerFunction`, `unstable_InstrumentRouterFunction`, `unstable_InstrumentRouteFunction`, and `unstable_InstrumentationHandlerResult` types have had their `unstable_` prefixes removed
diff --git a/packages/react-router/.changes/minor.stabilize-mask.md b/packages/react-router/.changes/minor.stabilize-mask.md
new file mode 100644
index 0000000000..0e9663fe0c
--- /dev/null
+++ b/packages/react-router/.changes/minor.stabilize-mask.md
@@ -0,0 +1 @@
+Stabilize `unstable_mask` as `mask` on ``, `useLinkClickHandler`, and `useNavigate`, and rename the corresponding `Location.unstable_mask` field to `Location.mask`
diff --git a/packages/react-router/.changes/minor.stabilize-normalize-path.md b/packages/react-router/.changes/minor.stabilize-normalize-path.md
new file mode 100644
index 0000000000..f9fcc61ec1
--- /dev/null
+++ b/packages/react-router/.changes/minor.stabilize-normalize-path.md
@@ -0,0 +1 @@
+Stabilize the `unstable_normalizePath` option on `staticHandler.query` and `staticHandler.queryRoute` as `normalizePath`
diff --git a/packages/react-router/.changes/minor.stabilize-pass-through-requests.md b/packages/react-router/.changes/minor.stabilize-pass-through-requests.md
new file mode 100644
index 0000000000..fa624d1d0f
--- /dev/null
+++ b/packages/react-router/.changes/minor.stabilize-pass-through-requests.md
@@ -0,0 +1 @@
+Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests`
diff --git a/packages/react-router/.changes/minor.stabilize-sri.md b/packages/react-router/.changes/minor.stabilize-sri.md
new file mode 100644
index 0000000000..9fef86d380
--- /dev/null
+++ b/packages/react-router/.changes/minor.stabilize-sri.md
@@ -0,0 +1 @@
+Remove `unstable_subResourceIntegrity` from the runtime `FutureConfig` type; the flag is now controlled by the top-level `subResourceIntegrity` option in `react-router.config.ts`
diff --git a/packages/react-router/.changes/minor.stabilize-url.md b/packages/react-router/.changes/minor.stabilize-url.md
new file mode 100644
index 0000000000..e14bee8989
--- /dev/null
+++ b/packages/react-router/.changes/minor.stabilize-url.md
@@ -0,0 +1 @@
+Stabilize `unstable_url` as `url` on `loader`, `action`, and `middleware` function args
diff --git a/packages/react-router/.changes/minor.stabilize-use-transitions.md b/packages/react-router/.changes/minor.stabilize-use-transitions.md
new file mode 100644
index 0000000000..38b60ffd8c
--- /dev/null
+++ b/packages/react-router/.changes/minor.stabilize-use-transitions.md
@@ -0,0 +1 @@
+Stabilize `unstable_useTransitions` as `useTransitions` on ``, ``, ``, ``, ``, ``, ``, and `useLinkClickHandler`
diff --git a/packages/react-router/.changes/patch.made-mask-optional-in-location.md b/packages/react-router/.changes/patch.made-mask-optional-in-location.md
new file mode 100644
index 0000000000..b2a92ebcf1
--- /dev/null
+++ b/packages/react-router/.changes/patch.made-mask-optional-in-location.md
@@ -0,0 +1 @@
+Mark `mask` as an optional field in `Location` for easier mocking in unit tests
diff --git a/packages/react-router/.changes/patch.made-unstablemask-optional-in-location.md b/packages/react-router/.changes/patch.made-unstablemask-optional-in-location.md
deleted file mode 100644
index 01cca2e592..0000000000
--- a/packages/react-router/.changes/patch.made-unstablemask-optional-in-location.md
+++ /dev/null
@@ -1 +0,0 @@
-Mark `unstable_mask` as an optional field in `Location` for easier mocking in unit tests
diff --git a/packages/react-router/__tests__/dom/client-on-error-test.tsx b/packages/react-router/__tests__/dom/client-on-error-test.tsx
index 917c2d882a..9a945c9321 100644
--- a/packages/react-router/__tests__/dom/client-on-error-test.tsx
+++ b/packages/react-router/__tests__/dom/client-on-error-test.tsx
@@ -51,7 +51,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("lazy error!"), {
       location: expect.objectContaining({ pathname: "/" }),
       params: {},
-      unstable_pattern: "/",
+      pattern: "/",
     });
     expect(spy).toHaveBeenCalledTimes(1);
     expect(getHtml(container)).toContain("Unexpected Application Error!");
@@ -83,7 +83,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("middleware error!"), {
       location: expect.objectContaining({ pathname: "/" }),
       params: {},
-      unstable_pattern: "/",
+      pattern: "/",
     });
     expect(spy).toHaveBeenCalledTimes(1);
   });
@@ -112,7 +112,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("loader error!"), {
       location: expect.objectContaining({ pathname: "/" }),
       params: {},
-      unstable_pattern: "/",
+      pattern: "/",
     });
     expect(spy).toHaveBeenCalledTimes(1);
   });
@@ -143,7 +143,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("lazy error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
     });
     expect(spy).toHaveBeenCalledTimes(1);
     let html = getHtml(container);
@@ -179,7 +179,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("middleware error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
     });
     expect(spy).toHaveBeenCalledTimes(1);
     expect(getHtml(container)).toContain("Error");
@@ -211,7 +211,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("loader error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
     });
     expect(spy).toHaveBeenCalledTimes(1);
     expect(getHtml(container)).toContain("Error");
@@ -248,7 +248,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("action error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
     });
     expect(spy).toHaveBeenCalledTimes(1);
     expect(getHtml(container)).toContain("Error");
@@ -278,7 +278,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("loader error!"), {
       location: expect.objectContaining({ pathname: "/" }),
       params: {},
-      unstable_pattern: "/",
+      pattern: "/",
     });
     expect(spy).toHaveBeenCalledTimes(1);
     expect(getHtml(container)).toContain("Error");
@@ -313,7 +313,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("action error!"), {
       location: expect.objectContaining({ pathname: "/" }),
       params: {},
-      unstable_pattern: "/",
+      pattern: "/",
     });
     expect(spy).toHaveBeenCalledTimes(1);
     expect(getHtml(container)).toContain("Error");
@@ -344,7 +344,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("render error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
       errorInfo: expect.objectContaining({
         componentStack: expect.any(String),
       }),
@@ -390,7 +390,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("await error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
     });
     expect(spy).toHaveBeenCalledTimes(1);
     expect(getHtml(container)).toContain("Await Error");
@@ -439,7 +439,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("await error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
       errorInfo: expect.objectContaining({
         componentStack: expect.any(String),
       }),
@@ -494,12 +494,12 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("await error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
     });
     expect(spy).toHaveBeenCalledWith(new Error("errorElement error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
       errorInfo: expect.objectContaining({
         componentStack: expect.any(String),
       }),
@@ -549,7 +549,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("loader error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
     });
     expect(spy).toHaveBeenCalledTimes(1);
     expect(getHtml(container)).toContain("Error");
@@ -600,7 +600,7 @@ describe(`handleError`, () => {
     expect(spy).toHaveBeenCalledWith(new Error("render error!"), {
       location: expect.objectContaining({ pathname: "/page" }),
       params: {},
-      unstable_pattern: "/page",
+      pattern: "/page",
       errorInfo: expect.objectContaining({
         componentStack: expect.any(String),
       }),
diff --git a/packages/react-router/__tests__/dom/data-browser-router-test.tsx b/packages/react-router/__tests__/dom/data-browser-router-test.tsx
index 49df600f23..b1dbfcee4c 100644
--- a/packages/react-router/__tests__/dom/data-browser-router-test.tsx
+++ b/packages/react-router/__tests__/dom/data-browser-router-test.tsx
@@ -2697,7 +2697,7 @@ function testDomRouter(
     });
 
     describe("call-site revalidation opt-out", () => {
-      it("accepts unstable_defaultShouldRevalidate on  navigations", async () => {
+      it("accepts defaultShouldRevalidate on  navigations", async () => {
         let loaderDefer = createDeferred();
 
         let router = createTestRouter(
@@ -2715,7 +2715,7 @@ function testDomRouter(
           let navigation = useNavigation();
           return (
             
- + Change Search Params
@@ -2761,7 +2761,7 @@ function testDomRouter( `); }); - it("accepts unstable_defaultShouldRevalidate on setSearchParams navigations", async () => { + it("accepts defaultShouldRevalidate on setSearchParams navigations", async () => { let loaderDefer = createDeferred(); let router = createTestRouter( @@ -2783,7 +2783,7 @@ function testDomRouter( @@ -2905,7 +2905,7 @@ function testDomRouter( `); }); - it("accepts unstable_defaultShouldRevalidate on fetcher.submit", async () => { + it("accepts defaultShouldRevalidate on fetcher.submit", async () => { let loaderDefer = createDeferred(); let actionDefer = createDeferred(); @@ -2937,7 +2937,7 @@ function testDomRouter( { method: "post", action: "/", - unstable_defaultShouldRevalidate: false, + defaultShouldRevalidate: false, }, ) } diff --git a/packages/react-router/__tests__/react-transitions-test.tsx b/packages/react-router/__tests__/react-transitions-test.tsx index e3d6a5e4b4..3235d6becc 100644 --- a/packages/react-router/__tests__/react-transitions-test.tsx +++ b/packages/react-router/__tests__/react-transitions-test.tsx @@ -29,7 +29,7 @@ import { createDeferred, tick } from "./router/utils/utils"; import getWindow from "./utils/getWindow"; describe("react transitions", () => { - describe("", () => { + describe("", () => { it("normal navigations surface all updates", async () => { let loaderDfd = createDeferred(); let router = createMemoryRouter([ @@ -333,7 +333,7 @@ describe("react transitions", () => { }); }); - describe("", () => { + describe("", () => { it("navigations are not transition-enabled", async () => { let loaderDfd = createDeferred(); let router = createMemoryRouter([ @@ -381,7 +381,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Go to page")); @@ -487,7 +487,7 @@ describe("react transitions", () => { }); }); - describe("", () => { + describe("", () => { it("Link navigations are transition-enabled", async () => { let loaderDfd = createDeferred(); let router = createMemoryRouter([ @@ -535,7 +535,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Go to page")); @@ -607,7 +607,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Go to page")); @@ -684,7 +684,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Go to page")); @@ -760,7 +760,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Submit")); @@ -843,7 +843,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Submit")); @@ -930,7 +930,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Submit")); @@ -1000,7 +1000,7 @@ describe("react transitions", () => { ); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Submit")); @@ -1056,7 +1056,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Fetch")); @@ -1120,7 +1120,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Fetch")); @@ -1191,7 +1191,7 @@ describe("react transitions", () => { ]); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Go to page")); @@ -1263,7 +1263,7 @@ describe("react transitions", () => { ); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Revalidate")); @@ -1338,7 +1338,7 @@ describe("react transitions", () => { ); let { container } = render( - , + , ); await waitFor(() => screen.getByText("Revalidate")); diff --git a/packages/react-router/__tests__/router/fetchers-test.ts b/packages/react-router/__tests__/router/fetchers-test.ts index a8bcf82c96..4f42d4a36c 100644 --- a/packages/react-router/__tests__/router/fetchers-test.ts +++ b/packages/react-router/__tests__/router/fetchers-test.ts @@ -177,8 +177,8 @@ describe("fetchers", () => { request: new Request("http://localhost/foo", { signal: A.loaders.foo.stub.mock.calls[0][0].request.signal, }), - unstable_pattern: "/foo", - unstable_url: new URL("http://localhost/foo"), + pattern: "/foo", + url: new URL("http://localhost/foo"), context: {}, }); }); @@ -226,8 +226,8 @@ describe("fetchers", () => { request: new Request("http://localhost/foo?key=value", { signal: A.loaders.foo.stub.mock.calls[0][0].request.signal, }), - unstable_pattern: "/foo", - unstable_url: new URL("http://localhost/foo?key=value"), + pattern: "/foo", + url: new URL("http://localhost/foo?key=value"), context: {}, }); @@ -280,8 +280,8 @@ describe("fetchers", () => { expect(A.actions.foo.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: "/foo", - unstable_url: new URL("http://localhost/foo"), + pattern: "/foo", + url: new URL("http://localhost/foo"), context: {}, }); @@ -393,8 +393,8 @@ describe("fetchers", () => { request: new Request("http://localhost/foo", { signal: A.loaders.root.stub.mock.calls[0][0].request.signal, }), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); }); @@ -3395,8 +3395,8 @@ describe("fetchers", () => { expect(F.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -3426,8 +3426,8 @@ describe("fetchers", () => { expect(F.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -3455,8 +3455,8 @@ describe("fetchers", () => { expect(F.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -3484,8 +3484,8 @@ describe("fetchers", () => { expect(F.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -3514,8 +3514,8 @@ describe("fetchers", () => { expect(F.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -3546,8 +3546,8 @@ describe("fetchers", () => { expect(F.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -3577,8 +3577,8 @@ describe("fetchers", () => { expect(F.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); diff --git a/packages/react-router/__tests__/router/instrumentation-test.ts b/packages/react-router/__tests__/router/instrumentation-test.ts index 52bab8a3b0..49d84ac871 100644 --- a/packages/react-router/__tests__/router/instrumentation-test.ts +++ b/packages/react-router/__tests__/router/instrumentation-test.ts @@ -43,7 +43,7 @@ describe("instrumentation", () => { loader: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -87,7 +87,7 @@ describe("instrumentation", () => { loader: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -135,7 +135,7 @@ describe("instrumentation", () => { index: "INDEX", }, }, - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -176,7 +176,7 @@ describe("instrumentation", () => { action: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -219,7 +219,7 @@ describe("instrumentation", () => { lazy: () => lazyDfd.promise, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -263,7 +263,7 @@ describe("instrumentation", () => { lazy: () => lazyDfd.promise, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -311,7 +311,7 @@ describe("instrumentation", () => { lazy: () => lazyDfd.promise, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -363,7 +363,7 @@ describe("instrumentation", () => { lazy: () => lazyDfd.promise, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -422,7 +422,7 @@ describe("instrumentation", () => { lazy: () => lazyDfd.promise, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -487,7 +487,7 @@ describe("instrumentation", () => { }, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -551,7 +551,7 @@ describe("instrumentation", () => { }, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -613,7 +613,7 @@ describe("instrumentation", () => { }, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -682,7 +682,7 @@ describe("instrumentation", () => { action: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -763,7 +763,7 @@ describe("instrumentation", () => { lazy: () => lazyDfd.promise, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -838,7 +838,7 @@ describe("instrumentation", () => { }, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -931,7 +931,7 @@ describe("instrumentation", () => { ]); } }, - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1009,7 +1009,7 @@ describe("instrumentation", () => { ]); } }, - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1090,7 +1090,7 @@ describe("instrumentation", () => { ]); } }, - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1169,7 +1169,7 @@ describe("instrumentation", () => { loader: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1228,7 +1228,7 @@ describe("instrumentation", () => { path: "/target", }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1295,7 +1295,7 @@ describe("instrumentation", () => { path: "/target", }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1365,7 +1365,7 @@ describe("instrumentation", () => { loader: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1408,7 +1408,7 @@ describe("instrumentation", () => { loader: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1451,7 +1451,7 @@ describe("instrumentation", () => { loader: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1488,7 +1488,7 @@ describe("instrumentation", () => { loader: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1530,7 +1530,7 @@ describe("instrumentation", () => { loader: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1554,7 +1554,7 @@ describe("instrumentation", () => { expect(args.request.headers.get).toBeDefined(); expect(args.request.headers.set).not.toBeDefined(); expect(args.params).toEqual({ slug: "a", extra: "extra" }); - expect(args.unstable_pattern).toBe("/:slug"); + expect(args.pattern).toBe("/:slug"); expect(args.context.get).toBeDefined(); expect(args.context.set).not.toBeDefined(); expect(t.router.state.matches[0].params).toEqual({ slug: "a" }); @@ -1573,7 +1573,7 @@ describe("instrumentation", () => { loader: true, }, ], - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1624,7 +1624,7 @@ describe("instrumentation", () => { }, ], { - unstable_instrumentations: [ + instrumentations: [ { router(router) { router.instrument({ @@ -1666,7 +1666,7 @@ describe("instrumentation", () => { }, ], { - unstable_instrumentations: [ + instrumentations: [ { router(router) { router.instrument({ @@ -1719,7 +1719,7 @@ describe("instrumentation", () => { }, ], { - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1766,7 +1766,7 @@ describe("instrumentation", () => { }, ], { - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1811,7 +1811,7 @@ describe("instrumentation", () => { }, ], { - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1879,7 +1879,7 @@ describe("instrumentation", () => { }, ], { - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1922,7 +1922,7 @@ describe("instrumentation", () => { }, ], { - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -1974,7 +1974,7 @@ describe("instrumentation", () => { handleDocumentRequest(request) { return new Response(`${request.method} ${request.url} COMPONENT`); }, - unstable_instrumentations: [ + instrumentations: [ { handler(handler) { handler.instrument({ @@ -2050,7 +2050,7 @@ describe("instrumentation", () => { handleDocumentRequest(request) { return new Response(`${request.method} ${request.url} COMPONENT`); }, - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -2081,7 +2081,7 @@ describe("instrumentation", () => { }, }, params: {}, - unstable_pattern: "/", + pattern: "/", context: { get: expect.any(Function), }, @@ -2100,7 +2100,7 @@ describe("instrumentation", () => { }, }, params: {}, - unstable_pattern: "/", + pattern: "/", context: { get: expect.any(Function), }, @@ -2126,7 +2126,7 @@ describe("instrumentation", () => { handleDocumentRequest(request) { return new Response(`${request.method} ${request.url} COMPONENT`); }, - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -2157,7 +2157,7 @@ describe("instrumentation", () => { }, }, params: {}, - unstable_pattern: "/", + pattern: "/", context: {}, }, ], @@ -2173,7 +2173,7 @@ describe("instrumentation", () => { }, }, params: {}, - unstable_pattern: "/", + pattern: "/", context: {}, }, ], @@ -2197,7 +2197,7 @@ describe("instrumentation", () => { handleDocumentRequest(request) { return new Response(`${request.method} ${request.url} COMPONENT`); }, - unstable_instrumentations: [ + instrumentations: [ { route(route) { route.instrument({ @@ -2230,7 +2230,7 @@ describe("instrumentation", () => { }, }, params: {}, - unstable_pattern: "/", + pattern: "/", context: {}, }, ], @@ -2246,7 +2246,7 @@ describe("instrumentation", () => { }, }, params: {}, - unstable_pattern: "/", + pattern: "/", context: {}, }, ], diff --git a/packages/react-router/__tests__/router/mask-test.ts b/packages/react-router/__tests__/router/mask-test.ts index 0ab7ea5444..70cb65f69e 100644 --- a/packages/react-router/__tests__/router/mask-test.ts +++ b/packages/react-router/__tests__/router/mask-test.ts @@ -16,7 +16,7 @@ describe("location masking", () => { // Navigate to /gallery?photo=123 but mask browser URL as /images/123 let A = await t.navigate("/gallery?photo=123", { - unstable_mask: "/images/123", + mask: "/images/123", }); // The loader should receive the router location URL in the request @@ -29,7 +29,7 @@ describe("location masking", () => { expect(t.router.state.location).toMatchObject({ pathname: "/gallery", search: "?photo=123", - unstable_mask: { + mask: { pathname: "/images/123", search: "", hash: "", @@ -54,7 +54,7 @@ describe("location masking", () => { // Navigate to /gallery?photo=123 with mask let A = await t.navigate("/gallery?photo=123", { - unstable_mask: "/images/123", + mask: "/images/123", }); await A.loaders.gallery.resolve("GALLERY DATA"); @@ -71,7 +71,7 @@ describe("location masking", () => { expect(t.router.state.location).toMatchObject({ pathname: "/gallery", search: "?photo=123", - unstable_mask: { + mask: { pathname: "/images/123", search: "", hash: "", @@ -91,7 +91,7 @@ describe("location masking", () => { }); let A = await t.navigate("/gallery?photo=123", { - unstable_mask: "/images/123", + mask: "/images/123", replace: true, }); @@ -101,7 +101,7 @@ describe("location masking", () => { expect(t.router.state.location).toMatchObject({ pathname: "/gallery", search: "?photo=123", - unstable_mask: { + mask: { pathname: "/images/123", search: "", hash: "", @@ -119,7 +119,7 @@ describe("location masking", () => { }); let A = await t.navigate("/gallery?photo=123#header", { - unstable_mask: "/images/123#preview", + mask: "/images/123#preview", }); expect(A.loaders.gallery).toBeDefined(); @@ -135,7 +135,7 @@ describe("location masking", () => { pathname: "/gallery", search: "?photo=123", hash: "#header", - unstable_mask: { + mask: { pathname: "/images/123", search: "", hash: "#preview", @@ -153,7 +153,7 @@ describe("location masking", () => { }); let A = await t.navigate("/gallery?photo=123", { - unstable_mask: "/images/123", + mask: "/images/123", state: { customData: "test" }, }); @@ -163,7 +163,7 @@ describe("location masking", () => { pathname: "/gallery", search: "?photo=123", state: { customData: "test" }, - unstable_mask: { + mask: { pathname: "/images/123", search: "", hash: "", @@ -181,7 +181,7 @@ describe("location masking", () => { }); let A = await t.navigate("/gallery?photo=123", { - unstable_mask: "/images/123", + mask: "/images/123", formMethod: "post", formData: createFormData({ test: "value" }), }); @@ -204,7 +204,7 @@ describe("location masking", () => { expect(t.router.state.location).toMatchObject({ pathname: "/gallery", search: "?photo=123", - unstable_mask: { + mask: { pathname: "/images/123", search: "", hash: "", @@ -226,7 +226,7 @@ describe("location masking", () => { }); let A = await t.navigate("/gallery?photo=123", { - unstable_mask: "/images/123", + mask: "/images/123", }); expect(A.loaders.gallery).toBeDefined(); @@ -235,7 +235,7 @@ describe("location masking", () => { expect(t.router.state.location).toMatchObject({ pathname: "/gallery", search: "?photo=123", - unstable_mask: { + mask: { pathname: "/images/123", search: "", hash: "", @@ -257,14 +257,14 @@ describe("location masking", () => { // Navigate to non-existent route with mask await t.navigate("/nonexistent", { - unstable_mask: "/images/123", + mask: "/images/123", }); // Should get a 404 error expect(t.router.state.errors).toBeDefined(); expect(t.router.state.location).toMatchObject({ pathname: "/nonexistent", - unstable_mask: { + mask: { pathname: "/images/123", search: "", hash: "", diff --git a/packages/react-router/__tests__/router/router-test.ts b/packages/react-router/__tests__/router/router-test.ts index 7bd315f1b1..1e808f1df1 100644 --- a/packages/react-router/__tests__/router/router-test.ts +++ b/packages/react-router/__tests__/router/router-test.ts @@ -1751,8 +1751,8 @@ describe("a router", () => { request: new Request("http://localhost/tasks", { signal: nav.loaders.tasks.stub.mock.calls[0][0].request.signal, }), - unstable_pattern: "/tasks", - unstable_url: new URL("http://localhost/tasks"), + pattern: "/tasks", + url: new URL("http://localhost/tasks"), context: {}, }); @@ -1762,8 +1762,8 @@ describe("a router", () => { request: new Request("http://localhost/tasks/1", { signal: nav2.loaders.tasksId.stub.mock.calls[0][0].request.signal, }), - unstable_pattern: "/tasks/:id", - unstable_url: new URL("http://localhost/tasks/1"), + pattern: "/tasks/:id", + url: new URL("http://localhost/tasks/1"), context: {}, }); @@ -1773,8 +1773,8 @@ describe("a router", () => { request: new Request("http://localhost/tasks?foo=bar", { signal: nav3.loaders.tasks.stub.mock.calls[0][0].request.signal, }), - unstable_pattern: "/tasks", - unstable_url: new URL("http://localhost/tasks?foo=bar#hash"), + pattern: "/tasks", + url: new URL("http://localhost/tasks?foo=bar#hash"), context: {}, }); @@ -1786,8 +1786,8 @@ describe("a router", () => { request: new Request("http://localhost/tasks?foo=bar", { signal: nav4.loaders.tasks.stub.mock.calls[0][0].request.signal, }), - unstable_pattern: "/tasks", - unstable_url: new URL("http://localhost/tasks?foo=bar#hash"), + pattern: "/tasks", + url: new URL("http://localhost/tasks?foo=bar#hash"), context: {}, }); @@ -2213,8 +2213,8 @@ describe("a router", () => { expect(nav.actions.tasks.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: "/tasks", - unstable_url: new URL("http://localhost/tasks"), + pattern: "/tasks", + url: new URL("http://localhost/tasks"), context: {}, }); @@ -2259,8 +2259,8 @@ describe("a router", () => { expect(nav.actions.tasks.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: "/tasks", - unstable_url: new URL("http://localhost/tasks?foo=bar"), + pattern: "/tasks", + url: new URL("http://localhost/tasks?foo=bar"), context: {}, }); // Assert request internals, cannot do a deep comparison above since some @@ -2294,8 +2294,8 @@ describe("a router", () => { expect(nav.actions.tasks.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); diff --git a/packages/react-router/__tests__/router/should-revalidate-test.ts b/packages/react-router/__tests__/router/should-revalidate-test.ts index 27b79e055a..707c270bfc 100644 --- a/packages/react-router/__tests__/router/should-revalidate-test.ts +++ b/packages/react-router/__tests__/router/should-revalidate-test.ts @@ -1251,7 +1251,7 @@ describe("shouldRevalidate", () => { }); let A = await t.navigate("/?foo=bar", { - unstable_defaultShouldRevalidate: false, + defaultShouldRevalidate: false, }); A.loaders.index.resolve("SHOULD NOT BE CALLED"); @@ -1290,7 +1290,7 @@ describe("shouldRevalidate", () => { }); let A = await t.navigate("/?foo=bar", { - unstable_defaultShouldRevalidate: false, + defaultShouldRevalidate: false, }); A.loaders.index.resolve("SHOULD NOT BE CALLED"); @@ -1345,7 +1345,7 @@ describe("shouldRevalidate", () => { { formMethod: "post", formData: createFormData({}), - unstable_defaultShouldRevalidate: false, + defaultShouldRevalidate: false, }, ["fetch"], ); @@ -1418,7 +1418,7 @@ describe("shouldRevalidate", () => { { formMethod: "post", formData: createFormData({}), - unstable_defaultShouldRevalidate: false, + defaultShouldRevalidate: false, }, ["fetch"], ); @@ -1483,7 +1483,7 @@ describe("shouldRevalidate", () => { let B = await t.fetch("/fetch", actionKey, "index", { formMethod: "post", formData: createFormData({}), - unstable_defaultShouldRevalidate: false, + defaultShouldRevalidate: false, }); t.shimHelper(B.loaders, "fetch", "loader", "fetch"); @@ -1552,7 +1552,7 @@ describe("shouldRevalidate", () => { let B = await t.fetch("/fetch", actionKey, "index", { formMethod: "post", formData: createFormData({}), - unstable_defaultShouldRevalidate: false, + defaultShouldRevalidate: false, }); t.shimHelper(B.loaders, "fetch", "loader", "fetch"); @@ -1623,7 +1623,7 @@ describe("shouldRevalidate", () => { let B = await t.fetch("/fetch", actionKey, "index", { formMethod: "post", formData: createFormData({}), - unstable_defaultShouldRevalidate: false, + defaultShouldRevalidate: false, }); t.shimHelper(B.loaders, "fetch", "loader", "fetch"); diff --git a/packages/react-router/__tests__/router/ssr-test.ts b/packages/react-router/__tests__/router/ssr-test.ts index 2cf9bd61c2..3aa9b11512 100644 --- a/packages/react-router/__tests__/router/ssr-test.ts +++ b/packages/react-router/__tests__/router/ssr-test.ts @@ -840,8 +840,8 @@ describe("ssr", () => { expect(rootLoaderStub).toHaveBeenCalledTimes(1); expect(rootLoaderStub).toHaveBeenCalledWith({ request: new Request("http://localhost/child"), - unstable_pattern: "/child", - unstable_url: new URL("http://localhost/child"), + pattern: "/child", + url: new URL("http://localhost/child"), params: {}, context: {}, }); @@ -853,8 +853,8 @@ describe("ssr", () => { expect(childLoaderStub).toHaveBeenCalledTimes(1); expect(childLoaderStub).toHaveBeenCalledWith({ request: new Request("http://localhost/child"), - unstable_pattern: "/child", - unstable_url: new URL("http://localhost/child"), + pattern: "/child", + url: new URL("http://localhost/child"), params: {}, context: {}, }); @@ -894,8 +894,8 @@ describe("ssr", () => { expect(actionStub).toHaveBeenCalledTimes(1); expect(actionStub).toHaveBeenCalledWith({ request: expect.any(Request), - unstable_pattern: "/child", - unstable_url: new URL("http://localhost/child"), + pattern: "/child", + url: new URL("http://localhost/child"), params: {}, context: {}, }); @@ -911,8 +911,8 @@ describe("ssr", () => { expect(rootLoaderStub).toHaveBeenCalledTimes(1); expect(rootLoaderStub).toHaveBeenCalledWith({ request: expect.any(Request), - unstable_pattern: "/child", - unstable_url: new URL("http://localhost/child"), + pattern: "/child", + url: new URL("http://localhost/child"), params: {}, context: {}, }); @@ -926,8 +926,8 @@ describe("ssr", () => { expect(childLoaderStub).toHaveBeenCalledTimes(1); expect(childLoaderStub).toHaveBeenCalledWith({ request: expect.any(Request), - unstable_pattern: "/child", - unstable_url: new URL("http://localhost/child"), + pattern: "/child", + url: new URL("http://localhost/child"), params: {}, context: {}, }); diff --git a/packages/react-router/__tests__/router/submission-test.ts b/packages/react-router/__tests__/router/submission-test.ts index fcfb3d369a..32774da019 100644 --- a/packages/react-router/__tests__/router/submission-test.ts +++ b/packages/react-router/__tests__/router/submission-test.ts @@ -948,8 +948,8 @@ describe("submissions", () => { expect(nav.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -984,8 +984,8 @@ describe("submissions", () => { expect(nav.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -1018,8 +1018,8 @@ describe("submissions", () => { expect(nav.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -1124,8 +1124,8 @@ describe("submissions", () => { expect(nav.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -1164,8 +1164,8 @@ describe("submissions", () => { expect(nav.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); @@ -1201,8 +1201,8 @@ describe("submissions", () => { expect(nav.actions.root.stub).toHaveBeenCalledWith({ params: {}, request: expect.any(Request), - unstable_pattern: expect.any(String), - unstable_url: expect.any(URL), + pattern: expect.any(String), + url: expect.any(URL), context: {}, }); diff --git a/packages/react-router/__tests__/server-runtime/utils.ts b/packages/react-router/__tests__/server-runtime/utils.ts index 10771ca1dc..fb5759a4fd 100644 --- a/packages/react-router/__tests__/server-runtime/utils.ts +++ b/packages/react-router/__tests__/server-runtime/utils.ts @@ -15,7 +15,7 @@ import type { LoaderFunction, MiddlewareFunction, } from "../../lib/router/utils"; -import type { unstable_ServerInstrumentation } from "../../lib/router/instrumentation"; +import type { ServerInstrumentation } from "../../lib/router/instrumentation"; export function mockServerBuild( routes: Record< @@ -36,14 +36,13 @@ export function mockServerBuild( future?: Partial; handleError?: HandleErrorFunction; handleDocumentRequest?: HandleDocumentRequestFunction; - unstable_instrumentations?: unstable_ServerInstrumentation[]; + instrumentations?: ServerInstrumentation[]; } = {}, ): ServerBuild { return { ssr: true, future: { v8_middleware: false, - unstable_subResourceIntegrity: false, ...opts.future, }, prerender: [], @@ -98,7 +97,7 @@ export function mockServerBuild( ), handleDataRequest: jest.fn(async (response) => response), handleError: opts.handleError, - unstable_instrumentations: opts.unstable_instrumentations, + instrumentations: opts.instrumentations, }, }, routes: Object.entries(routes).reduce( diff --git a/packages/react-router/__tests__/utils/framework.ts b/packages/react-router/__tests__/utils/framework.ts index 79e48247ed..78c677183e 100644 --- a/packages/react-router/__tests__/utils/framework.ts +++ b/packages/react-router/__tests__/utils/framework.ts @@ -31,7 +31,6 @@ export function mockFrameworkContext( }, future: { v8_middleware: false, - unstable_subResourceIntegrity: false, }, ssr: true, isSpaMode: false, diff --git a/packages/react-router/index.ts b/packages/react-router/index.ts index a398f1d0ed..5ebb3cfaef 100644 --- a/packages/react-router/index.ts +++ b/packages/react-router/index.ts @@ -71,12 +71,12 @@ export { parsePath, } from "./lib/router/history"; export type { - unstable_ServerInstrumentation, - unstable_ClientInstrumentation, - unstable_InstrumentRequestHandlerFunction, - unstable_InstrumentRouterFunction, - unstable_InstrumentRouteFunction, - unstable_InstrumentationHandlerResult, + ServerInstrumentation, + ClientInstrumentation, + InstrumentRequestHandlerFunction, + InstrumentRouterFunction, + InstrumentRouteFunction, + InstrumentationHandlerResult, } from "./lib/router/instrumentation"; export { IDLE_NAVIGATION, diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 59be609ead..282b1826b0 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -73,7 +73,7 @@ import { } from "./hooks"; import type { ViewTransition } from "./dom/global"; import { warnOnce } from "./server-runtime/warnings"; -import type { unstable_ClientInstrumentation } from "./router/instrumentation"; +import type { ClientInstrumentation } from "./router/instrumentation"; /** * Webpack can fail to compile against react versions without this export - @@ -211,7 +211,7 @@ export interface MemoryRouterOpts { * * ```tsx * let router = createBrowserRouter(routes, { - * unstable_instrumentations: [logging] + * instrumentations: [logging] * }); * * @@ -249,7 +249,7 @@ export interface MemoryRouterOpts { * } * ``` */ - unstable_instrumentations?: unstable_ClientInstrumentation[]; + instrumentations?: ClientInstrumentation[]; /** * Override the default data strategy of running loaders in parallel - * see the [docs](../../how-to/data-strategy) for more information. @@ -302,7 +302,7 @@ export interface MemoryRouterOpts { * @param {MemoryRouterOpts.hydrationData} opts.hydrationData n/a * @param {MemoryRouterOpts.initialEntries} opts.initialEntries n/a * @param {MemoryRouterOpts.initialIndex} opts.initialIndex n/a - * @param {MemoryRouterOpts.unstable_instrumentations} opts.unstable_instrumentations n/a + * @param {MemoryRouterOpts.instrumentations} opts.instrumentations n/a * @param {MemoryRouterOpts.patchRoutesOnNavigation} opts.patchRoutesOnNavigation n/a * @returns An initialized {@link DataRouter} to pass to {@link RouterProvider | ``} */ @@ -324,7 +324,7 @@ export function createMemoryRouter( mapRouteProperties, dataStrategy: opts?.dataStrategy, patchRoutesOnNavigation: opts?.patchRoutesOnNavigation, - unstable_instrumentations: opts?.unstable_instrumentations, + instrumentations: opts?.instrumentations, }).initialize(); } @@ -363,7 +363,7 @@ export interface ClientOnErrorFunction { info: { location: Location; params: Params; - unstable_pattern: string; + pattern: string; errorInfo?: React.ErrorInfo; }, ): void; @@ -399,7 +399,7 @@ export interface RouterProviderProps { * * ```tsx * { - * let { location, params, unstable_pattern, errorInfo } = info; + * let { location, params, pattern, errorInfo } = info; * console.error(error, location, errorInfo); * reportToErrorService(error, location, errorInfo); * }} /> @@ -422,9 +422,9 @@ export interface RouterProviderProps { * - When set to `false`, the router will not leverage `React.startTransition` or * `React.useOptimistic` on any navigations or state changes. * - * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). + * For more information, please see the [docs](../../explanation/react-transitions). */ - unstable_useTransitions?: boolean; + useTransitions?: boolean; } /** @@ -456,17 +456,17 @@ export interface RouterProviderProps { * @param {RouterProviderProps.flushSync} props.flushSync n/a * @param {RouterProviderProps.onError} props.onError n/a * @param {RouterProviderProps.router} props.router n/a - * @param {RouterProviderProps.unstable_useTransitions} props.unstable_useTransitions n/a + * @param {RouterProviderProps.useTransitions} props.useTransitions n/a * @returns React element for the rendered router */ export function RouterProvider({ router, flushSync: reactDomFlushSyncImpl, onError, - unstable_useTransitions, + useTransitions, }: RouterProviderProps): React.ReactElement { let unstable_rsc = useIsRSCRouterContext(); - unstable_useTransitions = unstable_rsc || unstable_useTransitions; + useTransitions = unstable_rsc || useTransitions; let [_state, setStateImpl] = React.useState(router.state); let [state, setOptimisticState] = useOptimisticSafe(_state); @@ -494,7 +494,7 @@ export function RouterProvider({ onError(error, { location: newState.location, params: newState.matches[0]?.params ?? {}, - unstable_pattern: getRoutePattern(newState.matches), + pattern: getRoutePattern(newState.matches), }), ); } @@ -533,11 +533,11 @@ export function RouterProvider({ if (!viewTransitionOpts || !isViewTransitionAvailable) { if (reactDomFlushSyncImpl && flushSync) { reactDomFlushSyncImpl(() => setStateImpl(newState)); - } else if (unstable_useTransitions === false) { + } else if (useTransitions === false) { setStateImpl(newState); } else { React.startTransition(() => { - if (unstable_useTransitions === true) { + if (useTransitions === true) { setOptimisticState((s) => getOptimisticRouterState(s, newState)); } setStateImpl(newState); @@ -609,7 +609,7 @@ export function RouterProvider({ reactDomFlushSyncImpl, transition, renderDfd, - unstable_useTransitions, + useTransitions, setOptimisticState, onError, ], @@ -649,11 +649,11 @@ export function RouterProvider({ let newState = pendingState; let renderPromise = renderDfd.promise; let transition = router.window.document.startViewTransition(async () => { - if (unstable_useTransitions === false) { + if (useTransitions === false) { setStateImpl(newState); } else { React.startTransition(() => { - if (unstable_useTransitions === true) { + if (useTransitions === true) { setOptimisticState((s) => getOptimisticRouterState(s, newState)); } setStateImpl(newState); @@ -673,7 +673,7 @@ export function RouterProvider({ pendingState, renderDfd, router.window, - unstable_useTransitions, + useTransitions, setOptimisticState, ]); @@ -753,7 +753,7 @@ export function RouterProvider({ location={state.location} navigationType={state.historyAction} navigator={navigator} - unstable_useTransitions={unstable_useTransitions} + useTransitions={useTransitions} > `} for client-side * routing. */ @@ -884,7 +884,7 @@ export function MemoryRouter({ children, initialEntries, initialIndex, - unstable_useTransitions, + useTransitions, }: MemoryRouterProps): React.ReactElement { let historyRef = React.useRef(); if (historyRef.current == null) { @@ -902,13 +902,13 @@ export function MemoryRouter({ }); let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - if (unstable_useTransitions === false) { + if (useTransitions === false) { setStateImpl(newState); } else { React.startTransition(() => setStateImpl(newState)); } }, - [unstable_useTransitions], + [useTransitions], ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -920,7 +920,7 @@ export function MemoryRouter({ location={state.location} navigationType={state.action} navigator={history} - unstable_useTransitions={unstable_useTransitions} + useTransitions={useTransitions} /> ); } @@ -1349,9 +1349,9 @@ export interface RouterProps { * - When set to `false`, the router will not leverage `React.startTransition` * on any navigations or state changes. * - * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). + * For more information, please see the [docs](../../explanation/react-transitions). */ - unstable_useTransitions?: boolean; + useTransitions?: boolean; } /** @@ -1371,7 +1371,7 @@ export interface RouterProps { * @param {RouterProps.navigationType} props.navigationType n/a * @param {RouterProps.navigator} props.navigator n/a * @param {RouterProps.static} props.static n/a - * @param {RouterProps.unstable_useTransitions} props.unstable_useTransitions n/a + * @param {RouterProps.useTransitions} props.useTransitions n/a * @returns React element for the rendered router or `null` if the location does * not match the {@link props.basename} */ @@ -1382,7 +1382,7 @@ export function Router({ navigationType = NavigationType.Pop, navigator, static: staticProp = false, - unstable_useTransitions, + useTransitions, }: RouterProps): React.ReactElement | null { invariant( !useInRouterContext(), @@ -1398,10 +1398,10 @@ export function Router({ basename, navigator, static: staticProp, - unstable_useTransitions, + useTransitions, future: {}, }), - [basename, navigator, staticProp, unstable_useTransitions], + [basename, navigator, staticProp, useTransitions], ); if (typeof locationProp === "string") { @@ -1414,7 +1414,7 @@ export function Router({ hash = "", state = null, key = "default", - unstable_mask, + mask, } = locationProp; let locationContext = React.useMemo(() => { @@ -1431,7 +1431,7 @@ export function Router({ hash, state, key, - unstable_mask, + mask, }, navigationType, }; @@ -1443,7 +1443,7 @@ export function Router({ state, key, navigationType, - unstable_mask, + mask, ]); warning( @@ -1684,7 +1684,7 @@ export function Await({ dataRouterContext.onError(error, { location: dataRouterStateContext.location, params: dataRouterStateContext.matches[0]?.params || {}, - unstable_pattern: getRoutePattern(dataRouterStateContext.matches), + pattern: getRoutePattern(dataRouterStateContext.matches), errorInfo, }); } diff --git a/packages/react-router/lib/context.ts b/packages/react-router/lib/context.ts index bdfb1ccef3..ef86a35db1 100644 --- a/packages/react-router/lib/context.ts +++ b/packages/react-router/lib/context.ts @@ -15,9 +15,9 @@ import type { TrackedPromise, RouteMatch } from "./router/utils"; export interface DataRouterContextObject // Omit `future` since those can be pulled from the `router` - // `NavigationContext` needs `future`/`unstable_useTransitions` since it doesn't + // `NavigationContext` needs `future`/`useTransitions` since it doesn't // have a `router` in all cases - extends Omit { + extends Omit { router: Router; staticContext?: StaticHandlerContext; onError?: ClientOnErrorFunction; @@ -74,7 +74,7 @@ export interface NavigateOptions { /** Replace the current entry in the history stack instead of pushing a new one */ replace?: boolean; /** Masked URL */ - unstable_mask?: To; + mask?: To; /** Adds persistent client side routing state to the next location */ state?: any; /** If you are using {@link ScrollRestoration ``}, prevent the scroll position from being reset to the top of the window when navigating */ @@ -86,7 +86,7 @@ export interface NavigateOptions { /** Enables a {@link https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API View Transition} for this navigation by wrapping the final state update in `document.startViewTransition()`. If you need to apply specific styles for this view transition, you will also need to leverage the {@link useViewTransitionState `useViewTransitionState()`} hook. */ viewTransition?: boolean; /** Specifies the default revalidation behavior after this submission */ - unstable_defaultShouldRevalidate?: boolean; + defaultShouldRevalidate?: boolean; } /** @@ -111,7 +111,7 @@ interface NavigationContextObject { basename: string; navigator: Navigator; static: boolean; - unstable_useTransitions: boolean | undefined; + useTransitions: boolean | undefined; // TODO: Re-introduce a singular `FutureConfig` once we land our first // future.unstable_ or future.v8_ flag future: {}; diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx index ddb69df172..47c5e2611c 100644 --- a/packages/react-router/lib/dom-export/hydrated-router.tsx +++ b/packages/react-router/lib/dom-export/hydrated-router.tsx @@ -27,7 +27,7 @@ import { } from "react-router"; import { CRITICAL_CSS_DATA_ATTRIBUTE } from "../dom/ssr/components"; import { RouterProvider } from "./dom-router-provider"; -import type { unstable_ClientInstrumentation } from "../router/instrumentation"; +import type { ClientInstrumentation } from "../router/instrumentation"; type SSRInfo = { context: NonNullable<(typeof window)["__reactRouterContext"]>; @@ -79,10 +79,10 @@ function initSsrInfo(): void { function createHydratedRouter({ getContext, - unstable_instrumentations, + instrumentations, }: { getContext?: RouterInit["getContext"]; - unstable_instrumentations?: unstable_ClientInstrumentation[]; + instrumentations?: ClientInstrumentation[]; }): DataRouter { initSsrInfo(); @@ -185,11 +185,11 @@ function createHydratedRouter({ getContext, hydrationData, hydrationRouteProperties, - unstable_instrumentations, + instrumentations, mapRouteProperties, future: { - unstable_passThroughRequests: - ssrInfo.context.future.unstable_passThroughRequests, + v8_passThroughRequests: + ssrInfo.context.future.v8_passThroughRequests, }, dataStrategy: getTurboStreamSingleFetchDataStrategy( () => router, @@ -287,12 +287,12 @@ export interface HydratedRouterProps { * startTransition(() => { * hydrateRoot( * document, - * + * * ); * }); * ``` */ - unstable_instrumentations?: unstable_ClientInstrumentation[]; + instrumentations?: ClientInstrumentation[]; /** * An error handler function that will be called for any middleware, loader, action, * or render errors that are encountered in your application. This is useful for @@ -305,7 +305,7 @@ export interface HydratedRouterProps { * * ```tsx * { - * let { location, params, unstable_pattern, errorInfo } = info; + * let { location, params, pattern, errorInfo } = info; * console.error(error, location, errorInfo); * reportToErrorService(error, location, errorInfo); * }} /> @@ -328,9 +328,9 @@ export interface HydratedRouterProps { * - When set to `false`, the router will not leverage `React.startTransition` or * `React.useOptimistic` on any navigations or state changes. * - * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). + * For more information, please see the [docs](../../explanation/react-transitions). */ - unstable_useTransitions?: boolean; + useTransitions?: boolean; } /** @@ -349,7 +349,7 @@ export function HydratedRouter(props: HydratedRouterProps) { if (!router) { router = createHydratedRouter({ getContext: props.getContext, - unstable_instrumentations: props.unstable_instrumentations, + instrumentations: props.instrumentations, }); } @@ -437,7 +437,7 @@ export function HydratedRouter(props: HydratedRouterProps) { diff --git a/packages/react-router/lib/dom/dom.ts b/packages/react-router/lib/dom/dom.ts index 476af2552d..562050adbe 100644 --- a/packages/react-router/lib/dom/dom.ts +++ b/packages/react-router/lib/dom/dom.ts @@ -204,7 +204,7 @@ interface SharedSubmitOptions { * By default (when not specified), loaders will revalidate according to the routers * standard revalidation behavior. */ - unstable_defaultShouldRevalidate?: boolean; + defaultShouldRevalidate?: boolean; } /** diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 8e371b6682..356aeefef7 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -95,7 +95,7 @@ import { useRouteId, } from "../hooks"; import type { SerializeFrom } from "../types/route-data"; -import type { unstable_ClientInstrumentation } from "../router/instrumentation"; +import type { ClientInstrumentation } from "../router/instrumentation"; import { escapeHtml } from "./ssr/markup"; //////////////////////////////////////////////////////////////////////////////// @@ -247,7 +247,7 @@ export interface DOMRouterOpts { * * ```tsx * let router = createBrowserRouter(routes, { - * unstable_instrumentations: [logging] + * instrumentations: [logging] * }); * * @@ -285,7 +285,7 @@ export interface DOMRouterOpts { * } * ``` */ - unstable_instrumentations?: unstable_ClientInstrumentation[]; + instrumentations?: ClientInstrumentation[]; /** * Override the default data strategy of running loaders in parallel - * see the [docs](../../how-to/data-strategy) for more information. @@ -635,7 +635,7 @@ export interface DOMRouterOpts { * @param {DOMRouterOpts.future} opts.future n/a * @param {DOMRouterOpts.getContext} opts.getContext n/a * @param {DOMRouterOpts.hydrationData} opts.hydrationData n/a - * @param {DOMRouterOpts.unstable_instrumentations} opts.unstable_instrumentations n/a + * @param {DOMRouterOpts.instrumentations} opts.instrumentations n/a * @param {DOMRouterOpts.patchRoutesOnNavigation} opts.patchRoutesOnNavigation n/a * @param {DOMRouterOpts.window} opts.window n/a * @returns An initialized {@link DataRouter| data router} to pass to {@link RouterProvider | ``} @@ -656,7 +656,7 @@ export function createBrowserRouter( dataStrategy: opts?.dataStrategy, patchRoutesOnNavigation: opts?.patchRoutesOnNavigation, window: opts?.window, - unstable_instrumentations: opts?.unstable_instrumentations, + instrumentations: opts?.instrumentations, }).initialize(); } @@ -673,7 +673,7 @@ export function createBrowserRouter( * @param {DOMRouterOpts.future} opts.future n/a * @param {DOMRouterOpts.getContext} opts.getContext n/a * @param {DOMRouterOpts.hydrationData} opts.hydrationData n/a - * @param {DOMRouterOpts.unstable_instrumentations} opts.unstable_instrumentations n/a + * @param {DOMRouterOpts.instrumentations} opts.instrumentations n/a * @param {DOMRouterOpts.dataStrategy} opts.dataStrategy n/a * @param {DOMRouterOpts.patchRoutesOnNavigation} opts.patchRoutesOnNavigation n/a * @param {DOMRouterOpts.window} opts.window n/a @@ -695,7 +695,7 @@ export function createHashRouter( dataStrategy: opts?.dataStrategy, patchRoutesOnNavigation: opts?.patchRoutesOnNavigation, window: opts?.window, - unstable_instrumentations: opts?.unstable_instrumentations, + instrumentations: opts?.instrumentations, }).initialize(); } @@ -788,9 +788,9 @@ export interface BrowserRouterProps { * - When set to `false`, the router will not leverage `React.startTransition` * on any navigations or state changes. * - * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). + * For more information, please see the [docs](../../explanation/react-transitions). */ - unstable_useTransitions?: boolean; + useTransitions?: boolean; /** * [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object * override. Defaults to the global `window` instance @@ -808,7 +808,7 @@ export interface BrowserRouterProps { * @param props Props * @param {BrowserRouterProps.basename} props.basename n/a * @param {BrowserRouterProps.children} props.children n/a - * @param {BrowserRouterProps.unstable_useTransitions} props.unstable_useTransitions n/a + * @param {BrowserRouterProps.useTransitions} props.useTransitions n/a * @param {BrowserRouterProps.window} props.window n/a * @returns A declarative {@link Router | ``} using the browser [`History`](https://developer.mozilla.org/en-US/docs/Web/API/History) * API for client-side routing. @@ -816,7 +816,7 @@ export interface BrowserRouterProps { export function BrowserRouter({ basename, children, - unstable_useTransitions, + useTransitions, window, }: BrowserRouterProps) { let historyRef = React.useRef(); @@ -831,13 +831,13 @@ export function BrowserRouter({ }); let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - if (unstable_useTransitions === false) { + if (useTransitions === false) { setStateImpl(newState); } else { React.startTransition(() => setStateImpl(newState)); } }, - [unstable_useTransitions], + [useTransitions], ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -849,7 +849,7 @@ export function BrowserRouter({ location={state.location} navigationType={state.action} navigator={history} - unstable_useTransitions={unstable_useTransitions} + useTransitions={useTransitions} /> ); } @@ -878,9 +878,9 @@ export interface HashRouterProps { * - When set to `false`, the router will not leverage `React.startTransition` * on any navigations or state changes. * - * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). + * For more information, please see the [docs](../../explanation/react-transitions). */ - unstable_useTransitions?: boolean; + useTransitions?: boolean; /** * [`Window`](https://developer.mozilla.org/en-US/docs/Web/API/Window) object * override. Defaults to the global `window` instance @@ -899,7 +899,7 @@ export interface HashRouterProps { * @param props Props * @param {HashRouterProps.basename} props.basename n/a * @param {HashRouterProps.children} props.children n/a - * @param {HashRouterProps.unstable_useTransitions} props.unstable_useTransitions n/a + * @param {HashRouterProps.useTransitions} props.useTransitions n/a * @param {HashRouterProps.window} props.window n/a * @returns A declarative {@link Router | ``} using the URL [`hash`](https://developer.mozilla.org/en-US/docs/Web/API/URL/hash) * for client-side routing. @@ -907,7 +907,7 @@ export interface HashRouterProps { export function HashRouter({ basename, children, - unstable_useTransitions, + useTransitions, window, }: HashRouterProps) { let historyRef = React.useRef(); @@ -922,13 +922,13 @@ export function HashRouter({ }); let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - if (unstable_useTransitions === false) { + if (useTransitions === false) { setStateImpl(newState); } else { React.startTransition(() => setStateImpl(newState)); } }, - [unstable_useTransitions], + [useTransitions], ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -940,7 +940,7 @@ export function HashRouter({ location={state.location} navigationType={state.action} navigator={history} - unstable_useTransitions={unstable_useTransitions} + useTransitions={useTransitions} /> ); } @@ -973,9 +973,9 @@ export interface HistoryRouterProps { * - When set to `false`, the router will not leverage `React.startTransition` * on any navigations or state changes. * - * For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). + * For more information, please see the [docs](../../explanation/react-transitions). */ - unstable_useTransitions?: boolean; + useTransitions?: boolean; } /** @@ -993,7 +993,7 @@ export interface HistoryRouterProps { * @param {HistoryRouterProps.basename} props.basename n/a * @param {HistoryRouterProps.children} props.children n/a * @param {HistoryRouterProps.history} props.history n/a - * @param {HistoryRouterProps.unstable_useTransitions} props.unstable_useTransitions n/a + * @param {HistoryRouterProps.useTransitions} props.useTransitions n/a * @returns A declarative {@link Router | ``} using the provided history * implementation for client-side routing. */ @@ -1001,7 +1001,7 @@ export function HistoryRouter({ basename, children, history, - unstable_useTransitions, + useTransitions, }: HistoryRouterProps) { let [state, setStateImpl] = React.useState({ action: history.action, @@ -1009,13 +1009,13 @@ export function HistoryRouter({ }); let setState = React.useCallback( (newState: { action: NavigationType; location: Location }) => { - if (unstable_useTransitions === false) { + if (useTransitions === false) { setStateImpl(newState); } else { React.startTransition(() => setStateImpl(newState)); } }, - [unstable_useTransitions], + [useTransitions], ); React.useLayoutEffect(() => history.listen(setState), [history, setState]); @@ -1027,7 +1027,7 @@ export function HistoryRouter({ location={state.location} navigationType={state.action} navigator={history} - unstable_useTransitions={unstable_useTransitions} + useTransitions={useTransitions} /> ); } @@ -1206,7 +1206,7 @@ export interface LinkProps * Specify the default revalidation behavior for the navigation. * * ```tsx - * + * * ``` * * If no `shouldRevalidate` functions are present on the active routes, then this @@ -1217,7 +1217,7 @@ export interface LinkProps * By default (when not specified), loaders will revalidate according to the routers * standard revalidation behavior. */ - unstable_defaultShouldRevalidate?: boolean; + defaultShouldRevalidate?: boolean; /** * Masked path for this navigation, when you want to navigate the router to @@ -1249,7 +1249,7 @@ export interface LinkProps * * {image.alt} * @@ -1266,7 +1266,7 @@ export interface LinkProps * } * ``` */ - unstable_mask?: To; + mask?: To; } const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; @@ -1299,8 +1299,8 @@ const ABSOLUTE_URL_REGEX = /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i; * @param {LinkProps.state} props.state n/a * @param {LinkProps.to} props.to n/a * @param {LinkProps.viewTransition} props.viewTransition [modes: framework, data] n/a - * @param {LinkProps.unstable_defaultShouldRevalidate} props.unstable_defaultShouldRevalidate n/a - * @param {LinkProps.unstable_mask} props.unstable_mask [modes: framework, data] n/a + * @param {LinkProps.defaultShouldRevalidate} props.defaultShouldRevalidate n/a + * @param {LinkProps.mask} props.mask [modes: framework, data] n/a */ export const Link = React.forwardRef( function LinkWithRef( @@ -1311,18 +1311,18 @@ export const Link = React.forwardRef( relative, reloadDocument, replace, - unstable_mask, + mask, state, target, to, preventScrollReset, viewTransition, - unstable_defaultShouldRevalidate, + defaultShouldRevalidate, ...rest }, forwardedRef, ) { - let { basename, navigator, unstable_useTransitions } = + let { basename, navigator, useTransitions } = React.useContext(NavigationContext); let isAbsolute = typeof to === "string" && ABSOLUTE_URL_REGEX.test(to); @@ -1335,13 +1335,13 @@ export const Link = React.forwardRef( let maskedHref: string | null = null; - if (unstable_mask) { + if (mask) { // Inlined version of the `useHref` logic operating off the masked location // instead of the current location let resolved = resolveTo( - unstable_mask, + mask, [], - location.unstable_mask ? location.unstable_mask.pathname : "/", + location.mask ? location.mask.pathname : "/", true, ); @@ -1366,14 +1366,14 @@ export const Link = React.forwardRef( let internalOnClick = useLinkClickHandler(to, { replace, - unstable_mask, + mask, state, target, preventScrollReset, relative, viewTransition, - unstable_defaultShouldRevalidate, - unstable_useTransitions, + defaultShouldRevalidate, + useTransitions, }); function handleClick( event: React.MouseEvent, @@ -1788,7 +1788,7 @@ interface SharedFormProps extends React.FormHTMLAttributes { * By default (when not specified), loaders will revalidate according to the routers * standard revalidation behavior. */ - unstable_defaultShouldRevalidate?: boolean; + defaultShouldRevalidate?: boolean; } /** @@ -1912,7 +1912,7 @@ type HTMLFormSubmitter = HTMLButtonElement | HTMLInputElement; * @param {FormProps.replace} replace n/a * @param {FormProps.state} state n/a * @param {FormProps.viewTransition} viewTransition n/a - * @param {FormProps.unstable_defaultShouldRevalidate} unstable_defaultShouldRevalidate n/a + * @param {FormProps.defaultShouldRevalidate} defaultShouldRevalidate n/a * @returns A progressively enhanced [`
`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form) component */ export const Form = React.forwardRef( @@ -1930,12 +1930,12 @@ export const Form = React.forwardRef( relative, preventScrollReset, viewTransition, - unstable_defaultShouldRevalidate, + defaultShouldRevalidate, ...props }, forwardedRef, ) => { - let { unstable_useTransitions } = React.useContext(NavigationContext); + let { useTransitions } = React.useContext(NavigationContext); let submit = useSubmit(); let formAction = useFormAction(action, { relative }); let formMethod: HTMLFormMethod = @@ -1965,10 +1965,10 @@ export const Form = React.forwardRef( relative, preventScrollReset, viewTransition, - unstable_defaultShouldRevalidate, + defaultShouldRevalidate, }); - if (unstable_useTransitions && navigate !== false) { + if (useTransitions && navigate !== false) { // @ts-expect-error Needs React 19 types React.startTransition(() => doSubmit()); } else { @@ -2188,11 +2188,11 @@ function useDataRouterState(hookName: DataRouterStateHook) { * @param options.viewTransition Enables a [View Transition](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) * for this navigation. To apply specific styles during the transition, see * {@link useViewTransitionState}. Defaults to `false`. - * @param options.unstable_defaultShouldRevalidate Specify the default revalidation + * @param options.defaultShouldRevalidate Specify the default revalidation * behavior for the navigation. Defaults to `true`. - * @param options.unstable_mask Masked location to display in the browser instead + * @param options.mask Masked location to display in the browser instead * of the router location. Defaults to `undefined`. - * @param options.unstable_useTransitions Wraps the navigation in + * @param options.useTransitions Wraps the navigation in * [`React.startTransition`](https://react.dev/reference/react/startTransition) * for concurrent rendering. Defaults to `false`. * @returns A click handler function that can be used in a custom {@link Link} component. @@ -2202,23 +2202,23 @@ export function useLinkClickHandler( { target, replace: replaceProp, - unstable_mask, + mask, state, preventScrollReset, relative, viewTransition, - unstable_defaultShouldRevalidate, - unstable_useTransitions, + defaultShouldRevalidate, + useTransitions, }: { target?: React.HTMLAttributeAnchorTarget; replace?: boolean; - unstable_mask?: To; + mask?: To; state?: any; preventScrollReset?: boolean; relative?: RelativeRoutingType; viewTransition?: boolean; - unstable_defaultShouldRevalidate?: boolean; - unstable_useTransitions?: boolean; + defaultShouldRevalidate?: boolean; + useTransitions?: boolean; } = {}, ): (event: React.MouseEvent) => void { let navigate = useNavigate(); @@ -2240,15 +2240,15 @@ export function useLinkClickHandler( let doNavigate = () => navigate(to, { replace, - unstable_mask, + mask, state, preventScrollReset, relative, viewTransition, - unstable_defaultShouldRevalidate, + defaultShouldRevalidate, }); - if (unstable_useTransitions) { + if (useTransitions) { // @ts-expect-error Needs React 19 types React.startTransition(() => doNavigate()); } else { @@ -2261,15 +2261,15 @@ export function useLinkClickHandler( navigate, path, replaceProp, - unstable_mask, + mask, state, target, to, preventScrollReset, relative, viewTransition, - unstable_defaultShouldRevalidate, - unstable_useTransitions, + defaultShouldRevalidate, + useTransitions, ], ); } @@ -2591,8 +2591,8 @@ export function useSubmit(): SubmitFunction { if (options.navigate === false) { let key = options.fetcherKey || getUniqueFetcherId(); await routerFetch(key, currentRouteId, options.action || action, { - unstable_defaultShouldRevalidate: - options.unstable_defaultShouldRevalidate, + defaultShouldRevalidate: + options.defaultShouldRevalidate, preventScrollReset: options.preventScrollReset, formData, body, @@ -2602,8 +2602,8 @@ export function useSubmit(): SubmitFunction { }); } else { await routerNavigate(options.action || action, { - unstable_defaultShouldRevalidate: - options.unstable_defaultShouldRevalidate, + defaultShouldRevalidate: + options.defaultShouldRevalidate, preventScrollReset: options.preventScrollReset, formData, body, diff --git a/packages/react-router/lib/dom/server.tsx b/packages/react-router/lib/dom/server.tsx index d148f6b19a..a7decf3237 100644 --- a/packages/react-router/lib/dom/server.tsx +++ b/packages/react-router/lib/dom/server.tsx @@ -86,7 +86,7 @@ export function StaticRouter({ hash: locationProp.hash || "", state: locationProp.state != null ? locationProp.state : null, key: locationProp.key || "default", - unstable_mask: undefined, + mask: undefined, }; let staticNavigator = getStatelessNavigator(); @@ -98,7 +98,7 @@ export function StaticRouter({ navigationType={action} navigator={staticNavigator} static={true} - unstable_useTransitions={false} + useTransitions={false} /> ); } @@ -209,7 +209,7 @@ export function StaticRouterProvider({ navigationType={state.historyAction} navigator={dataRouterContext.navigator} static={dataRouterContext.static} - unstable_useTransitions={false} + useTransitions={false} > { @@ -364,8 +364,8 @@ export function createClientRoutes( request, params, context, - unstable_pattern, - unstable_url, + pattern, + url, async serverLoader() { preventInvalidServerHandlerCall("loader", route); @@ -405,8 +405,8 @@ export function createClientRoutes( request, params, context, - unstable_pattern, - unstable_url, + pattern, + url, }: ActionFunctionArgs, singleFetch?: unknown, ) => { @@ -426,8 +426,8 @@ export function createClientRoutes( request, params, context, - unstable_pattern, - unstable_url, + pattern, + url, async serverAction() { preventInvalidServerHandlerCall("action", route); return fetchServerAction(singleFetch); diff --git a/packages/react-router/lib/hooks.tsx b/packages/react-router/lib/hooks.tsx index ea5fa50c4b..f75de69745 100644 --- a/packages/react-router/lib/hooks.tsx +++ b/packages/react-router/lib/hooks.tsx @@ -947,7 +947,7 @@ export function useRoutesImpl( hash: "", state: null, key: "default", - unstable_mask: undefined, + mask: undefined, ...location, }, navigationType: NavigationType.Pop, @@ -1294,7 +1294,7 @@ export function _renderMatches( onErrorHandler(error, { location: dataRouterState.location, params: dataRouterState.matches?.[0]?.params ?? {}, - unstable_pattern: getRoutePattern(dataRouterState.matches), + pattern: getRoutePattern(dataRouterState.matches), errorInfo, }); } diff --git a/packages/react-router/lib/router/history.ts b/packages/react-router/lib/router/history.ts index 9dd10ab943..44dd8eff81 100644 --- a/packages/react-router/lib/router/history.ts +++ b/packages/react-router/lib/router/history.ts @@ -74,7 +74,7 @@ export interface Location extends Path { * The masked location displayed in the URL bar, which differs from the URL the * router is operating on */ - unstable_mask?: Path; + mask?: Path; } /** @@ -256,7 +256,7 @@ export function createMemoryHistory( entry, typeof entry === "string" ? null : entry.state, index === 0 ? "default" : undefined, - typeof entry === "string" ? undefined : entry.unstable_mask, + typeof entry === "string" ? undefined : entry.mask, ), ); let index = clampIndex( @@ -275,14 +275,14 @@ export function createMemoryHistory( to: To, state: any = null, key?: string, - unstable_mask?: Path, + mask?: Path, ): Location { let location = createLocation( entries ? getCurrentLocation().pathname : "/", to, state, key, - unstable_mask, + mask, ); warning( location.pathname.charAt(0) === "/", @@ -552,7 +552,7 @@ function getHistoryState(location: Location, index: number): HistoryState { usr: location.state, key: location.key, idx: index, - masked: location.unstable_mask + masked: location.mask ? { pathname: location.pathname, search: location.search, @@ -570,7 +570,7 @@ export function createLocation( to: To, state: any = null, key?: string, - unstable_mask?: Path, + mask?: Path, ): Readonly { let location: Readonly = { pathname: typeof current === "string" ? current : current.pathname, @@ -583,7 +583,7 @@ export function createLocation( // But that's a pretty big refactor to the current test suite so going to // keep as is for the time being and just let any incoming keys take precedence key: (to && (to as Location).key) || key || createKey(), - unstable_mask, + mask, }; return location; } @@ -685,7 +685,7 @@ function getUrlBasedHistory( index = getIndex() + 1; let historyState = getHistoryState(location, index); - let url = history.createHref(location.unstable_mask || location); + let url = history.createHref(location.mask || location); // try...catch because iOS limits us to 100 pushState calls :/ try { @@ -717,7 +717,7 @@ function getUrlBasedHistory( index = getIndex(); let historyState = getHistoryState(location, index); - let url = history.createHref(location.unstable_mask || location); + let url = history.createHref(location.mask || location); globalHistory.replaceState(historyState, "", url); if (v5Compat && listener) { diff --git a/packages/react-router/lib/router/instrumentation.ts b/packages/react-router/lib/router/instrumentation.ts index 43fef3185e..697a782cf5 100644 --- a/packages/react-router/lib/router/instrumentation.ts +++ b/packages/react-router/lib/router/instrumentation.ts @@ -18,35 +18,35 @@ import type { } from "./utils"; // Public APIs -export type unstable_ServerInstrumentation = { - handler?: unstable_InstrumentRequestHandlerFunction; - route?: unstable_InstrumentRouteFunction; +export type ServerInstrumentation = { + handler?: InstrumentRequestHandlerFunction; + route?: InstrumentRouteFunction; }; -export type unstable_ClientInstrumentation = { - router?: unstable_InstrumentRouterFunction; - route?: unstable_InstrumentRouteFunction; +export type ClientInstrumentation = { + router?: InstrumentRouterFunction; + route?: InstrumentRouteFunction; }; -export type unstable_InstrumentRequestHandlerFunction = ( +export type InstrumentRequestHandlerFunction = ( handler: InstrumentableRequestHandler, ) => void; -export type unstable_InstrumentRouterFunction = ( +export type InstrumentRouterFunction = ( router: InstrumentableRouter, ) => void; -export type unstable_InstrumentRouteFunction = ( +export type InstrumentRouteFunction = ( route: InstrumentableRoute, ) => void; -export type unstable_InstrumentationHandlerResult = +export type InstrumentationHandlerResult = | { status: "success"; error: undefined } | { status: "error"; error: Error }; // Shared type InstrumentFunction = ( - handler: () => Promise, + handler: () => Promise, info: T, ) => Promise; @@ -90,7 +90,7 @@ type RouteLazyInstrumentationInfo = undefined; type RouteHandlerInstrumentationInfo = Readonly<{ request: ReadonlyRequest; params: LoaderFunctionArgs["params"]; - unstable_pattern: string; + pattern: string; context: ReadonlyContext; }>; @@ -140,7 +140,7 @@ type RequestHandlerInstrumentationInfo = Readonly<{ const UninstrumentedSymbol = Symbol("Uninstrumented"); export function getRouteInstrumentationUpdates( - fns: unstable_InstrumentRouteFunction[], + fns: InstrumentRouteFunction[], route: Readonly, ) { let aggregated: { @@ -255,7 +255,7 @@ export function getRouteInstrumentationUpdates( export function instrumentClientSideRouter( router: Router, - fns: unstable_InstrumentRouterFunction[], + fns: InstrumentRouterFunction[], ): Router { let aggregated: { navigate: InstrumentFunction[]; @@ -327,7 +327,7 @@ export function instrumentClientSideRouter( export function instrumentHandler( handler: RequestHandler, - fns: unstable_InstrumentRequestHandlerFunction[], + fns: InstrumentRequestHandlerFunction[], ): RequestHandler { let aggregated: { request: InstrumentFunction[]; @@ -407,7 +407,7 @@ async function recurseRight( // handler, we need to ensure the handlers still gets called let handlerPromise: ReturnType | undefined = undefined; let callHandler = - async (): Promise => { + async (): Promise => { if (handlerPromise) { console.error("You cannot call instrumented handlers more than once"); } else { @@ -451,11 +451,11 @@ function getHandlerInfo( | ActionFunctionArgs | Parameters[0], ): RouteHandlerInstrumentationInfo { - let { request, context, params, unstable_pattern } = args; + let { request, context, params, pattern } = args; return { request: getReadonlyRequest(request), params: { ...params }, - unstable_pattern, + pattern, context: getReadonlyContext(context), }; } diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 8641b99f32..c7b3d6a4c4 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -9,10 +9,10 @@ import { warning, } from "./history"; import type { - unstable_ClientInstrumentation, - unstable_InstrumentRouteFunction, - unstable_InstrumentRouterFunction, - unstable_ServerInstrumentation, + ClientInstrumentation, + InstrumentRouteFunction, + InstrumentRouterFunction, + ServerInstrumentation, } from "./instrumentation"; import { getRouteInstrumentationUpdates, @@ -426,9 +426,7 @@ export type HydrationState = Partial< /** * Future flags to toggle new feature behavior */ -export interface FutureConfig { - unstable_passThroughRequests: boolean; -} +export interface FutureConfig {} /** * Initialization options for createRouter @@ -438,7 +436,7 @@ export interface RouterInit { history: History; basename?: string; getContext?: () => MaybePromise; - unstable_instrumentations?: unstable_ClientInstrumentation[]; + instrumentations?: ClientInstrumentation[]; mapRouteProperties?: MapRoutePropertiesFunction; future?: Partial; hydrationRouteProperties?: string[]; @@ -493,7 +491,7 @@ export interface StaticHandler { * @param opts.requestContext Context object to pass to loaders/actions * @param opts.skipLoaderErrorBubbling Skip loader error bubbling * @param opts.skipRevalidation Skip revalidation after action submission - * @param opts.unstable_normalizePath Normalize the request path + * @param opts.normalizePath Normalize the request path */ query( request: Request, @@ -511,7 +509,7 @@ export interface StaticHandler { }, ) => Promise, ) => MaybePromise; - unstable_normalizePath?: (request: Request) => Path; + normalizePath?: (request: Request) => Path; }, ): Promise; /** @@ -524,7 +522,7 @@ export interface StaticHandler { * to generate a response to bubble back up the middleware chain * @param opts.requestContext Context object to pass to loaders/actions * @param opts.routeId The ID of the route to query - * @param opts.unstable_normalizePath Normalize the request path + * @param opts.normalizePath Normalize the request path */ queryRoute( @@ -536,7 +534,7 @@ export interface StaticHandler { generateMiddlewareResponse?: ( queryRoute: (r: Request) => Promise, ) => MaybePromise; - unstable_normalizePath?: (request: Request) => Path; + normalizePath?: (request: Request) => Path; }, ): Promise; } @@ -591,7 +589,7 @@ type BaseNavigateOrFetchOptions = { preventScrollReset?: boolean; relative?: RelativeRoutingType; flushSync?: boolean; - unstable_defaultShouldRevalidate?: boolean; + defaultShouldRevalidate?: boolean; }; // Only allowed for navigations @@ -600,7 +598,7 @@ type BaseNavigateOptions = BaseNavigateOrFetchOptions & { state?: any; fromRouteId?: string; viewTransition?: boolean; - unstable_mask?: To; + mask?: To; }; // Only allowed for submission navigations @@ -1006,8 +1004,8 @@ export function createRouter(init: RouterInit): Router { // Leverage the existing mapRouteProperties logic to execute instrumentRoute // (if it exists) on all routes in the application - if (init.unstable_instrumentations) { - let instrumentations = init.unstable_instrumentations; + if (init.instrumentations) { + let instrumentations = init.instrumentations; mapRouteProperties = (route: DataRouteObject) => { return { @@ -1015,7 +1013,7 @@ export function createRouter(init: RouterInit): Router { ...getRouteInstrumentationUpdates( instrumentations .map((i) => i.route) - .filter(Boolean) as unstable_InstrumentRouteFunction[], + .filter(Boolean) as InstrumentRouteFunction[], route, ), }; @@ -1042,7 +1040,6 @@ export function createRouter(init: RouterInit): Router { // Config driven behavior flags let future: FutureConfig = { - unstable_passThroughRequests: false, ...init.future, }; @@ -1658,13 +1655,13 @@ export function createRouter(init: RouterInit): Router { // If mask is provided, normalize and create a separate path for the router let maskPath: Path | undefined; - if (opts?.unstable_mask) { + if (opts?.mask) { let partialPath = - typeof opts.unstable_mask === "string" - ? parsePath(opts.unstable_mask) + typeof opts.mask === "string" + ? parsePath(opts.mask) : { - ...state.location.unstable_mask, - ...opts.unstable_mask, + ...state.location.mask, + ...opts.mask, }; maskPath = { pathname: "", @@ -1759,8 +1756,7 @@ export function createRouter(init: RouterInit): Router { replace: opts && opts.replace, enableViewTransition: opts && opts.viewTransition, flushSync, - callSiteDefaultShouldRevalidate: - opts && opts.unstable_defaultShouldRevalidate, + callSiteDefaultShouldRevalidate: opts && opts.defaultShouldRevalidate, }); } @@ -2585,7 +2581,7 @@ export function createRouter(init: RouterInit): Router { flushSync, preventScrollReset, submission, - opts && opts.unstable_defaultShouldRevalidate, + opts && opts.defaultShouldRevalidate, ); return; } @@ -3882,12 +3878,12 @@ export function createRouter(init: RouterInit): Router { }, }; - if (init.unstable_instrumentations) { + if (init.instrumentations) { router = instrumentClientSideRouter( router, - init.unstable_instrumentations + init.instrumentations .map((i) => i.router) - .filter(Boolean) as unstable_InstrumentRouterFunction[], + .filter(Boolean) as InstrumentRouterFunction[], ); } @@ -3902,7 +3898,7 @@ export function createRouter(init: RouterInit): Router { export interface CreateStaticHandlerOptions { basename?: string; mapRouteProperties?: MapRoutePropertiesFunction; - unstable_instrumentations?: Pick[]; + instrumentations?: Pick[]; future?: Partial; } @@ -3923,14 +3919,13 @@ export function createStaticHandler( // Currently unused in the static handler, but available for additional flags in the future // eslint-disable-next-line @typescript-eslint/no-unused-vars let future: FutureConfig = { - unstable_passThroughRequests: false, // unused in static handler ...opts?.future, }; // Leverage the existing mapRouteProperties logic to execute instrumentRoute // (if it exists) on all routes in the application - if (opts?.unstable_instrumentations) { - let instrumentations = opts.unstable_instrumentations; + if (opts?.instrumentations) { + let instrumentations = opts.instrumentations; mapRouteProperties = (route: DataRouteObject) => { return { @@ -3938,7 +3933,7 @@ export function createStaticHandler( ...getRouteInstrumentationUpdates( instrumentations .map((i) => i.route) - .filter(Boolean) as unstable_InstrumentRouteFunction[], + .filter(Boolean) as InstrumentRouteFunction[], route, ), }; @@ -3990,12 +3985,17 @@ export function createStaticHandler( skipRevalidation, dataStrategy, generateMiddlewareResponse, - unstable_normalizePath, + normalizePath, }: Parameters[1] = {}, ): Promise { - let normalizePath = unstable_normalizePath || defaultNormalizePath; + let normalizePathImpl = normalizePath || defaultNormalizePath; let method = request.method; - let location = createLocation("", normalizePath(request), null, "default"); + let location = createLocation( + "", + normalizePathImpl(request), + null, + "default", + ); let matches = matchRoutesImpl( dataRoutes, location, @@ -4080,8 +4080,8 @@ export function createStaticHandler( let response = await runServerMiddlewarePipeline( { request, - unstable_url: createDataFunctionUrl(request, location), - unstable_pattern: getRoutePattern(matches), + url: createDataFunctionUrl(request, location), + pattern: getRoutePattern(matches), matches, params: matches[0].params, // If we're calling middleware then it must be enabled so we can cast @@ -4273,12 +4273,17 @@ export function createStaticHandler( requestContext, dataStrategy, generateMiddlewareResponse, - unstable_normalizePath, + normalizePath, }: Parameters[1] = {}, ): Promise { - let normalizePath = unstable_normalizePath || defaultNormalizePath; + let normalizePathImpl = normalizePath || defaultNormalizePath; let method = request.method; - let location = createLocation("", normalizePath(request), null, "default"); + let location = createLocation( + "", + normalizePathImpl(request), + null, + "default", + ); let matches = matchRoutesImpl( dataRoutes, location, @@ -4320,8 +4325,8 @@ export function createStaticHandler( let response = await runServerMiddlewarePipeline( { request, - unstable_url: createDataFunctionUrl(request, location), - unstable_pattern: getRoutePattern(matches), + url: createDataFunctionUrl(request, location), + pattern: getRoutePattern(matches), matches, params: matches[0].params, // If we're calling middleware then it must be enabled so we can cast @@ -6223,7 +6228,7 @@ function getDataStrategyMatch( manifest: RouteManifest, request: Request, path: To, - unstable_pattern: string, + pattern: string, match: DataRouteMatch, lazyRoutePropertiesToSkip: string[], scopedContext: unknown, @@ -6294,7 +6299,7 @@ function getDataStrategyMatch( return callLoaderOrAction({ request, path, - unstable_pattern, + pattern, match, lazyHandlerPromise: _lazyPromises?.handler, lazyRoutePromise: _lazyPromises?.route, @@ -6375,8 +6380,8 @@ async function callDataStrategyImpl( "fetcherKey" | "runClientMiddleware" > = { request, - unstable_url: createDataFunctionUrl(request, path), - unstable_pattern: getRoutePattern(matches), + url: createDataFunctionUrl(request, path), + pattern: getRoutePattern(matches), params: matches[0].params, context: scopedContext, matches, @@ -6435,7 +6440,7 @@ async function callDataStrategyImpl( async function callLoaderOrAction({ request, path, - unstable_pattern, + pattern, match, lazyHandlerPromise, lazyRoutePromise, @@ -6444,7 +6449,7 @@ async function callLoaderOrAction({ }: { request: Request; path: To; - unstable_pattern: string; + pattern: string; match: DataRouteMatch; lazyHandlerPromise: Promise | undefined; lazyRoutePromise: Promise | undefined; @@ -6478,8 +6483,8 @@ async function callLoaderOrAction({ return handler( { request, - unstable_url: createDataFunctionUrl(request, path), - unstable_pattern, + url: createDataFunctionUrl(request, path), + pattern, params: match.params, context: scopedContext, }, @@ -6778,7 +6783,7 @@ function createClientSideRequest( return new Request(url, init); } -// Create the unstable_url object to pass to loaders/actions/middleware. +// Create the normalized URL instance to pass to loaders/actions/middleware. // We strip the `?index` param because that is a React Router implementation detail. function createDataFunctionUrl(request: Request, path: To): URL { let url = new URL(request.url); diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 52139a7b74..9e4b4f4029 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -271,19 +271,19 @@ interface DataFunctionArgs { /** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read headers (like cookies, and {@link https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams URLSearchParams} from the request. */ request: Request; /** - * A URL instance representing the application location being navigated to or fetched. - * Without `future.unstable_passThroughRequests` enabled, this matches `request.url`. - * With `future.unstable_passThroughRequests` enabled, this is a normalized - * URL with React-Router-specific implementation details removed (`.data` - * suffixes, `index`/`_routes` search params). - * The URL includes the origin from the request for convenience. + * A URL instance representing the application location being navigated to or + * fetched. By default, this matches `request.url`. + * + * In Framework mode with `future.v8_passThroughRequests` enabled, this is a + * normalized URL with React-Router-specific implementation details removed + * (`.data` suffixes, `index`/`_routes` search params). */ - unstable_url: URL; + url: URL; /** * Matched un-interpolated route pattern for the current path (i.e., /blog/:slug). * Mostly useful as a identifier to aggregate on for logging/tracing/etc. */ - unstable_pattern: string; + pattern: string; /** * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. * @example @@ -776,7 +776,7 @@ type Regex_w = Regex_az | Regex_AZ | Regex_09 | "_"; // prettier-ignore /** Emulates Regex `+` operator */ -type RegexMatchPlus = +type RegexMatchPlus = _RegexMatchPlus extends infer result extends string ? result extends '' ? never : result : diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index f7ff676a03..7d1b7656dd 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -840,9 +840,8 @@ export function RSCHydratedRouter({ // These flags have no runtime impact so can always be false. If we add // flags that drive runtime behavior they'll need to be proxied through. v8_middleware: false, - unstable_subResourceIntegrity: false, unstable_trailingSlashAwareDataRequests: true, // always on for RSC - unstable_passThroughRequests: true, // always on for RSC + v8_passThroughRequests: true, // always on for RSC }, isSpaMode: false, ssr: true, diff --git a/packages/react-router/lib/rsc/server.rsc.ts b/packages/react-router/lib/rsc/server.rsc.ts index 51e00d74dc..69a6dab548 100644 --- a/packages/react-router/lib/rsc/server.rsc.ts +++ b/packages/react-router/lib/rsc/server.rsc.ts @@ -748,7 +748,7 @@ async function generateResourceResponse( return generateErrorResponse(error); } }, - unstable_normalizePath: (r) => getNormalizedPath(r, basename, null), + normalizePath: (r) => getNormalizedPath(r, basename, null), }); return response; } catch (error) { @@ -841,7 +841,7 @@ async function generateRenderResponse( ...(routeIdsToLoad ? { filterMatchesToLoad: (m) => routeIdsToLoad!.includes(m.route.id) } : {}), - unstable_normalizePath: (r) => getNormalizedPath(r, basename, null), + normalizePath: (r) => getNormalizedPath(r, basename, null), async generateMiddlewareResponse(query) { // If this is an RSC server action, process that and then call query as a // revalidation. If this is a RR Form/Fetcher submission, diff --git a/packages/react-router/lib/rsc/server.ssr.tsx b/packages/react-router/lib/rsc/server.ssr.tsx index 328023f2d8..2952f38501 100644 --- a/packages/react-router/lib/rsc/server.ssr.tsx +++ b/packages/react-router/lib/rsc/server.ssr.tsx @@ -581,9 +581,8 @@ export function RSCStaticRouter({ getPayload }: RSCStaticRouterProps) { // These flags have no runtime impact so can always be false. If we add // flags that drive runtime behavior they'll need to be proxied through. v8_middleware: false, - unstable_subResourceIntegrity: false, unstable_trailingSlashAwareDataRequests: true, // always on for RSC - unstable_passThroughRequests: true, // always on for RSC + v8_passThroughRequests: true, // always on for RSC }, isSpaMode: false, ssr: true, diff --git a/packages/react-router/lib/server-runtime/build.ts b/packages/react-router/lib/server-runtime/build.ts index 4a1a6200e3..199ee6f40b 100644 --- a/packages/react-router/lib/server-runtime/build.ts +++ b/packages/react-router/lib/server-runtime/build.ts @@ -12,7 +12,7 @@ import type { import type { ServerRouteManifest } from "./routes"; import type { AppLoadContext } from "./data"; import type { MiddlewareEnabled } from "../types/future"; -import type { unstable_ServerInstrumentation } from "../router/instrumentation"; +import type { ServerInstrumentation } from "../router/instrumentation"; type OptionalCriticalCss = CriticalCss | undefined; @@ -87,6 +87,6 @@ export interface ServerEntryModule { default: HandleDocumentRequestFunction; handleDataRequest?: HandleDataRequestFunction; handleError?: HandleErrorFunction; - unstable_instrumentations?: unstable_ServerInstrumentation[]; + instrumentations?: ServerInstrumentation[]; streamTimeout?: number; } diff --git a/packages/react-router/lib/server-runtime/data.ts b/packages/react-router/lib/server-runtime/data.ts index 3d75870aaa..3a97d3ebcb 100644 --- a/packages/react-router/lib/server-runtime/data.ts +++ b/packages/react-router/lib/server-runtime/data.ts @@ -4,8 +4,8 @@ import type { LoaderFunctionArgs, ActionFunctionArgs, } from "../router/utils"; -import type { FutureConfig } from "../router/router"; import { isDataWithResponseInit, isRedirectStatusCode } from "../router/router"; +import type { FutureConfig } from "../dom/ssr/entry"; /** * An object of unknown type for route loaders and actions provided by the @@ -25,13 +25,13 @@ export async function callRouteHandler( future: FutureConfig, ) { let result = await handler({ - request: future.unstable_passThroughRequests + request: future.v8_passThroughRequests ? args.request : stripRoutesParam(stripIndexParam(args.request)), - unstable_url: args.unstable_url, + url: args.url, params: args.params, context: args.context, - unstable_pattern: args.unstable_pattern, + pattern: args.pattern, }); // If they returned a redirect via data(), re-throw it as a Response diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts index e00418b558..8e1b2fe69f 100644 --- a/packages/react-router/lib/server-runtime/server.ts +++ b/packages/react-router/lib/server-runtime/server.ts @@ -40,7 +40,7 @@ import { getDocumentHeaders } from "./headers"; import type { EntryRoute } from "../dom/ssr/routes"; import type { MiddlewareEnabled } from "../types/future"; import { URL_LIMIT, getManifestPath } from "../dom/ssr/fog-of-war"; -import type { unstable_InstrumentRequestHandlerFunction } from "../router/instrumentation"; +import type { InstrumentRequestHandlerFunction } from "../router/instrumentation"; import { instrumentHandler } from "../router/instrumentation"; import { throwIfPotentialCSRFAttack } from "../actions"; import { getNormalizedPath } from "./urls"; @@ -62,7 +62,7 @@ function derive(build: ServerBuild, mode?: string) { let serverMode = isServerMode(mode) ? mode : ServerMode.Production; let staticHandler = createStaticHandler(dataRoutes, { basename: build.basename, - unstable_instrumentations: build.entry.module.unstable_instrumentations, + instrumentations: build.entry.module.instrumentations, future: build.future, }); @@ -304,12 +304,12 @@ function derive(build: ServerBuild, mode?: string) { return response; }; - if (build.entry.module.unstable_instrumentations) { + if (build.entry.module.instrumentations) { requestHandler = instrumentHandler( requestHandler, - build.entry.module.unstable_instrumentations + build.entry.module.instrumentations .map((i) => i.handler) - .filter(Boolean) as unstable_InstrumentRequestHandlerFunction[], + .filter(Boolean) as InstrumentRequestHandlerFunction[], ); } @@ -510,7 +510,7 @@ async function handleDocumentRequest( } } : undefined, - unstable_normalizePath: (r) => + normalizePath: (r) => getNormalizedPath(r, build.basename, build.future), }); @@ -690,7 +690,7 @@ async function handleResourceRequest( } } : undefined, - unstable_normalizePath: (r) => + normalizePath: (r) => getNormalizedPath(r, build.basename, build.future), }); diff --git a/packages/react-router/lib/server-runtime/single-fetch.ts b/packages/react-router/lib/server-runtime/single-fetch.ts index 5289a0b5fe..2cf4527318 100644 --- a/packages/react-router/lib/server-runtime/single-fetch.ts +++ b/packages/react-router/lib/server-runtime/single-fetch.ts @@ -55,7 +55,7 @@ export async function singleFetchAction( return handleQueryError(new Error("Bad Request"), 400); } - let handlerRequest = build.future.unstable_passThroughRequests + let handlerRequest = build.future.v8_passThroughRequests ? request : new Request(handlerUrl, { method: request.method, @@ -79,7 +79,7 @@ export async function singleFetchAction( } } : undefined, - unstable_normalizePath: (r) => + normalizePath: (r) => getNormalizedPath(r, build.basename, build.future), }); @@ -152,7 +152,7 @@ export async function singleFetchLoaders( let loadRouteIds = routesParam ? new Set(routesParam.split(",")) : null; try { - let handlerRequest = build.future.unstable_passThroughRequests + let handlerRequest = build.future.v8_passThroughRequests ? request : new Request(handlerUrl, { headers: request.headers, @@ -173,7 +173,7 @@ export async function singleFetchLoaders( } } : undefined, - unstable_normalizePath: (r) => + normalizePath: (r) => getNormalizedPath(r, build.basename, build.future), }); diff --git a/packages/react-router/lib/types/route-data.ts b/packages/react-router/lib/types/route-data.ts index b4bacc336d..30ae969bee 100644 --- a/packages/react-router/lib/types/route-data.ts +++ b/packages/react-router/lib/types/route-data.ts @@ -79,14 +79,14 @@ export type ClientDataFunctionArgs = { **/ request: Request; /** - * A URL instance representing the application location being navigated to or fetched. - * Without `future.unstable_passThroughRequests` enabled, this matches `request.url`. - * With `future.unstable_passThroughRequests` enabled, this is a normalized - * URL with React-Router-specific implementation details removed (`.data` - * pathnames, `index`/`_routes` search params). - * The URL includes the origin from the request for convenience. + * A URL instance representing the application location being navigated to or + * fetched. By default, this matches `request.url`. + * + * In Framework mode with `future.v8_passThroughRequests` enabled, this is a + * normalized URL with React-Router-specific implementation details removed + * (`.data` suffixes, `index`/`_routes` search params). */ - unstable_url: URL; + url: URL; /** * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. * @example @@ -106,7 +106,7 @@ export type ClientDataFunctionArgs = { * Matched un-interpolated route pattern for the current path (i.e., /blog/:slug). * Mostly useful as a identifier to aggregate on for logging/tracing/etc. */ - unstable_pattern: string; + pattern: string; /** * When `future.v8_middleware` is not enabled, this is undefined. * @@ -122,14 +122,14 @@ export type ServerDataFunctionArgs = { /** A {@link https://developer.mozilla.org/en-US/docs/Web/API/Request Fetch Request instance} which you can use to read the url, method, headers (such as cookies), and request body from the request. */ request: Request; /** - * A URL instance representing the application location being navigated to or fetched. - * Without `future.unstable_passThroughRequests` enabled, this matches `request.url`. - * With `future.unstable_passThroughRequests` enabled, this is a normalized - * URL with React-Router-specific implementation details removed (`.data` - * pathnames, `index`/`_routes` search params). - * The URL includes the origin from the request for convenience. + * A URL instance representing the application location being navigated to or + * fetched. By default, this matches `request.url`. + * + * In Framework mode with `future.v8_passThroughRequests` enabled, this is a + * normalized URL with React-Router-specific implementation details removed + * (`.data` suffixes, `index`/`_routes` search params). */ - unstable_url: URL; + url: URL; /** * {@link https://reactrouter.com/start/framework/routing#dynamic-segments Dynamic route params} for the current route. * @example @@ -149,7 +149,7 @@ export type ServerDataFunctionArgs = { * Matched un-interpolated route pattern for the current path (i.e., /blog/:slug). * Mostly useful as a identifier to aggregate on for logging/tracing/etc. */ - unstable_pattern: string; + pattern: string; /** * Without `future.v8_middleware` enabled, this is the context passed in * to your server adapter's `getLoadContext` function. It's a way to bridge the diff --git a/playground/framework-vite-6/react-router.config.ts b/playground/framework-vite-6/react-router.config.ts index d0da26c798..7ce2466dc9 100644 --- a/playground/framework-vite-6/react-router.config.ts +++ b/playground/framework-vite-6/react-router.config.ts @@ -1,7 +1,5 @@ import type { Config } from "@react-router/dev/config"; export default { - future: { - unstable_subResourceIntegrity: true, - }, + subResourceIntegrity: true, } satisfies Config; diff --git a/playground/framework-vite-7-beta/react-router.config.ts b/playground/framework-vite-7-beta/react-router.config.ts index d19bb05394..c66d89deba 100644 --- a/playground/framework-vite-7-beta/react-router.config.ts +++ b/playground/framework-vite-7-beta/react-router.config.ts @@ -1,8 +1,8 @@ import type { Config } from "@react-router/dev/config"; export default { + subResourceIntegrity: true, future: { - unstable_subResourceIntegrity: true, unstable_optimizeDeps: true, v8_viteEnvironmentApi: true, }, diff --git a/playground/framework/react-router.config.ts b/playground/framework/react-router.config.ts index d19bb05394..c66d89deba 100644 --- a/playground/framework/react-router.config.ts +++ b/playground/framework/react-router.config.ts @@ -1,8 +1,8 @@ import type { Config } from "@react-router/dev/config"; export default { + subResourceIntegrity: true, future: { - unstable_subResourceIntegrity: true, unstable_optimizeDeps: true, v8_viteEnvironmentApi: true, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab9fdf7b9b..4a2e547a06 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2022,6 +2022,9 @@ importers: '@octokit/request': specifier: ^9.1.3 version: 9.2.4 + react-router: + specifier: workspace:^ + version: link:../packages/react-router semver: specifier: ^7.7.2 version: 7.7.4 diff --git a/scripts/bench/passthrough-requests.bench.mjs b/scripts/bench/passthrough-requests.bench.mjs index 4b38c124f1..bc764c7981 100644 --- a/scripts/bench/passthrough-requests.bench.mjs +++ b/scripts/bench/passthrough-requests.bench.mjs @@ -1,5 +1,5 @@ /** - * Benchmark: unstable_passThroughRequests flag + * Benchmark: passThroughRequests flag * * Measures the overhead of creating new Request objects on each server handler * invocation (default behavior) vs passing the original request through. @@ -13,7 +13,7 @@ */ import { bench, describe } from "vitest"; -import { createRequestHandler } from "../../packages/react-router/dist/production/index.js"; +import { createRequestHandler } from "react-router"; // --------------------------------------------------------------------------- // Minimal server build factory @@ -24,12 +24,7 @@ function mockServerBuild(future = {}) { return { ssr: true, - future: { - v8_middleware: false, - unstable_subResourceIntegrity: false, - unstable_passThroughRequests: false, - ...future, - }, + future, prerender: [], isSpaMode: false, routeDiscovery: { mode: "lazy", manifestPath: "/__manifest" }, @@ -64,8 +59,6 @@ function mockServerBuild(future = {}) { default: async (_request, statusCode, headers) => new Response(null, { status: statusCode, headers }), handleDataRequest: async (response) => response, - handleError: undefined, - unstable_instrumentations: undefined, }, }, routes: { @@ -91,12 +84,12 @@ function mockServerBuild(future = {}) { // --------------------------------------------------------------------------- const handlerDefault = createRequestHandler( - mockServerBuild({ unstable_passThroughRequests: false }), + mockServerBuild({ v8_passThroughRequests: false }), "production", ); const handlerPassThrough = createRequestHandler( - mockServerBuild({ unstable_passThroughRequests: true }), + mockServerBuild({ v8_passThroughRequests: true }), "production", ); diff --git a/scripts/package.json b/scripts/package.json index 7509e889af..406777e2d3 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -4,6 +4,7 @@ "type": "module", "dependencies": { "@octokit/request": "^9.1.3", + "react-router": "workspace:^", "semver": "^7.7.2" }, "devDependencies": { From e756132b5bcae52b65c504bdac8cec406c746e62 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 4 May 2026 18:18:26 +0000 Subject: [PATCH 14/23] chore: format --- docs/how-to/instrumentation.md | 10 ++---- packages/react-router/lib/components.tsx | 11 +----- .../lib/dom-export/hydrated-router.tsx | 3 +- packages/react-router/lib/dom/lib.tsx | 6 ++-- .../lib/dom/ssr/routes-test-stub.tsx | 3 +- packages/react-router/lib/dom/ssr/routes.tsx | 16 ++------- .../lib/router/instrumentation.ts | 35 ++++++++----------- .../react-router/lib/server-runtime/server.ts | 6 ++-- .../lib/server-runtime/single-fetch.ts | 6 ++-- 9 files changed, 28 insertions(+), 68 deletions(-) diff --git a/docs/how-to/instrumentation.md b/docs/how-to/instrumentation.md index adf5defc49..ed57f5bd76 100644 --- a/docs/how-to/instrumentation.md +++ b/docs/how-to/instrumentation.md @@ -135,11 +135,7 @@ startTransition(() => { hydrateRoot( document, - + , ); }); @@ -267,9 +263,7 @@ export const instrumentations = [ ]; // Framework Mode (entry.client.tsx) -; +; // Data Mode const router = createBrowserRouter(routes, { diff --git a/packages/react-router/lib/components.tsx b/packages/react-router/lib/components.tsx index 282b1826b0..5234145af9 100644 --- a/packages/react-router/lib/components.tsx +++ b/packages/react-router/lib/components.tsx @@ -1435,16 +1435,7 @@ export function Router({ }, navigationType, }; - }, [ - basename, - pathname, - search, - hash, - state, - key, - navigationType, - mask, - ]); + }, [basename, pathname, search, hash, state, key, navigationType, mask]); warning( locationContext != null, diff --git a/packages/react-router/lib/dom-export/hydrated-router.tsx b/packages/react-router/lib/dom-export/hydrated-router.tsx index 47c5e2611c..a27ac7fbc3 100644 --- a/packages/react-router/lib/dom-export/hydrated-router.tsx +++ b/packages/react-router/lib/dom-export/hydrated-router.tsx @@ -188,8 +188,7 @@ function createHydratedRouter({ instrumentations, mapRouteProperties, future: { - v8_passThroughRequests: - ssrInfo.context.future.v8_passThroughRequests, + v8_passThroughRequests: ssrInfo.context.future.v8_passThroughRequests, }, dataStrategy: getTurboStreamSingleFetchDataStrategy( () => router, diff --git a/packages/react-router/lib/dom/lib.tsx b/packages/react-router/lib/dom/lib.tsx index 356aeefef7..9f6dde80bf 100644 --- a/packages/react-router/lib/dom/lib.tsx +++ b/packages/react-router/lib/dom/lib.tsx @@ -2591,8 +2591,7 @@ export function useSubmit(): SubmitFunction { if (options.navigate === false) { let key = options.fetcherKey || getUniqueFetcherId(); await routerFetch(key, currentRouteId, options.action || action, { - defaultShouldRevalidate: - options.defaultShouldRevalidate, + defaultShouldRevalidate: options.defaultShouldRevalidate, preventScrollReset: options.preventScrollReset, formData, body, @@ -2602,8 +2601,7 @@ export function useSubmit(): SubmitFunction { }); } else { await routerNavigate(options.action || action, { - defaultShouldRevalidate: - options.defaultShouldRevalidate, + defaultShouldRevalidate: options.defaultShouldRevalidate, preventScrollReset: options.preventScrollReset, formData, body, diff --git a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx index de162a3e18..66f0c5c3cf 100644 --- a/packages/react-router/lib/dom/ssr/routes-test-stub.tsx +++ b/packages/react-router/lib/dom/ssr/routes-test-stub.tsx @@ -126,8 +126,7 @@ export function createRoutesStub( if (routerRef.current == null) { frameworkContextRef.current = { future: { - v8_passThroughRequests: - future?.v8_passThroughRequests === true, + v8_passThroughRequests: future?.v8_passThroughRequests === true, v8_middleware: future?.v8_middleware === true, unstable_trailingSlashAwareDataRequests: future?.unstable_trailingSlashAwareDataRequests === true, diff --git a/packages/react-router/lib/dom/ssr/routes.tsx b/packages/react-router/lib/dom/ssr/routes.tsx index 0fb6dd935f..e32fe658df 100644 --- a/packages/react-router/lib/dom/ssr/routes.tsx +++ b/packages/react-router/lib/dom/ssr/routes.tsx @@ -340,13 +340,7 @@ export function createClientRoutes( (routeModule.clientLoader?.hydrate === true || !route.hasLoader); dataRoute.loader = async ( - { - request, - params, - context, - pattern, - url, - }: LoaderFunctionArgs, + { request, params, context, pattern, url }: LoaderFunctionArgs, singleFetch?: unknown, ) => { try { @@ -401,13 +395,7 @@ export function createClientRoutes( ); dataRoute.action = ( - { - request, - params, - context, - pattern, - url, - }: ActionFunctionArgs, + { request, params, context, pattern, url }: ActionFunctionArgs, singleFetch?: unknown, ) => { return prefetchStylesAndCallHandler(async () => { diff --git a/packages/react-router/lib/router/instrumentation.ts b/packages/react-router/lib/router/instrumentation.ts index 697a782cf5..0efe1222ff 100644 --- a/packages/react-router/lib/router/instrumentation.ts +++ b/packages/react-router/lib/router/instrumentation.ts @@ -32,13 +32,9 @@ export type InstrumentRequestHandlerFunction = ( handler: InstrumentableRequestHandler, ) => void; -export type InstrumentRouterFunction = ( - router: InstrumentableRouter, -) => void; +export type InstrumentRouterFunction = (router: InstrumentableRouter) => void; -export type InstrumentRouteFunction = ( - route: InstrumentableRoute, -) => void; +export type InstrumentRouteFunction = (route: InstrumentableRoute) => void; export type InstrumentationHandlerResult = | { status: "success"; error: undefined } @@ -406,20 +402,19 @@ async function recurseRight( // If they forget to call the handler, or if they throw before calling the // handler, we need to ensure the handlers still gets called let handlerPromise: ReturnType | undefined = undefined; - let callHandler = - async (): Promise => { - if (handlerPromise) { - console.error("You cannot call instrumented handlers more than once"); - } else { - handlerPromise = recurseRight(impls, info, handler, index - 1); - } - result = await handlerPromise; - invariant(result, "Expected a result"); - if (result.type === "error" && result.value instanceof Error) { - return { status: "error", error: result.value }; - } - return { status: "success", error: undefined }; - }; + let callHandler = async (): Promise => { + if (handlerPromise) { + console.error("You cannot call instrumented handlers more than once"); + } else { + handlerPromise = recurseRight(impls, info, handler, index - 1); + } + result = await handlerPromise; + invariant(result, "Expected a result"); + if (result.type === "error" && result.value instanceof Error) { + return { status: "error", error: result.value }; + } + return { status: "success", error: undefined }; + }; try { await impl(callHandler, info); diff --git a/packages/react-router/lib/server-runtime/server.ts b/packages/react-router/lib/server-runtime/server.ts index 8e1b2fe69f..ae90e90c00 100644 --- a/packages/react-router/lib/server-runtime/server.ts +++ b/packages/react-router/lib/server-runtime/server.ts @@ -510,8 +510,7 @@ async function handleDocumentRequest( } } : undefined, - normalizePath: (r) => - getNormalizedPath(r, build.basename, build.future), + normalizePath: (r) => getNormalizedPath(r, build.basename, build.future), }); if (!isResponse(result)) { @@ -690,8 +689,7 @@ async function handleResourceRequest( } } : undefined, - normalizePath: (r) => - getNormalizedPath(r, build.basename, build.future), + normalizePath: (r) => getNormalizedPath(r, build.basename, build.future), }); return handleQueryRouteResult(result); diff --git a/packages/react-router/lib/server-runtime/single-fetch.ts b/packages/react-router/lib/server-runtime/single-fetch.ts index 2cf4527318..63679304c3 100644 --- a/packages/react-router/lib/server-runtime/single-fetch.ts +++ b/packages/react-router/lib/server-runtime/single-fetch.ts @@ -79,8 +79,7 @@ export async function singleFetchAction( } } : undefined, - normalizePath: (r) => - getNormalizedPath(r, build.basename, build.future), + normalizePath: (r) => getNormalizedPath(r, build.basename, build.future), }); return handleQueryResult(result); @@ -173,8 +172,7 @@ export async function singleFetchLoaders( } } : undefined, - normalizePath: (r) => - getNormalizedPath(r, build.basename, build.future), + normalizePath: (r) => getNormalizedPath(r, build.basename, build.future), }); return handleQueryResult(result); From 35dccb4ed1feb3989e54634cff4dcb49bfba786c Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Mon, 4 May 2026 18:20:47 +0000 Subject: [PATCH 15/23] chore: generate markdown docs from jsdocs --- docs/api/data-routers/RouterProvider.md | 2 +- docs/api/declarative-routers/BrowserRouter.md | 2 +- docs/api/declarative-routers/HashRouter.md | 2 +- docs/api/declarative-routers/HistoryRouter.md | 2 +- docs/api/declarative-routers/MemoryRouter.md | 2 +- docs/api/declarative-routers/Router.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/api/data-routers/RouterProvider.md b/docs/api/data-routers/RouterProvider.md index 57aa0bcbee..afd33ce036 100644 --- a/docs/api/data-routers/RouterProvider.md +++ b/docs/api/data-routers/RouterProvider.md @@ -105,5 +105,5 @@ Control whether router state updates are internally wrapped in - When set to `false`, the router will not leverage `React.startTransition` or `React.useOptimistic` on any navigations or state changes. -For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). +For more information, please see the [docs](../../explanation/react-transitions). diff --git a/docs/api/declarative-routers/BrowserRouter.md b/docs/api/declarative-routers/BrowserRouter.md index a0ad59eff9..8515cee98f 100644 --- a/docs/api/declarative-routers/BrowserRouter.md +++ b/docs/api/declarative-routers/BrowserRouter.md @@ -59,7 +59,7 @@ Control whether router state updates are internally wrapped in - When set to `false`, the router will not leverage `React.startTransition` on any navigations or state changes. -For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). +For more information, please see the [docs](../../explanation/react-transitions). ### window diff --git a/docs/api/declarative-routers/HashRouter.md b/docs/api/declarative-routers/HashRouter.md index b67677ed7d..0a899a7658 100644 --- a/docs/api/declarative-routers/HashRouter.md +++ b/docs/api/declarative-routers/HashRouter.md @@ -60,7 +60,7 @@ Control whether router state updates are internally wrapped in - When set to `false`, the router will not leverage `React.startTransition` on any navigations or state changes. -For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). +For more information, please see the [docs](../../explanation/react-transitions). ### window diff --git a/docs/api/declarative-routers/HistoryRouter.md b/docs/api/declarative-routers/HistoryRouter.md index 522d950062..5a7a99665c 100644 --- a/docs/api/declarative-routers/HistoryRouter.md +++ b/docs/api/declarative-routers/HistoryRouter.md @@ -74,5 +74,5 @@ Control whether router state updates are internally wrapped in - When set to `false`, the router will not leverage `React.startTransition` on any navigations or state changes. -For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). +For more information, please see the [docs](../../explanation/react-transitions). diff --git a/docs/api/declarative-routers/MemoryRouter.md b/docs/api/declarative-routers/MemoryRouter.md index c04ccc39b2..ddd57f09c8 100644 --- a/docs/api/declarative-routers/MemoryRouter.md +++ b/docs/api/declarative-routers/MemoryRouter.md @@ -67,5 +67,5 @@ Control whether router state updates are internally wrapped in - When set to `false`, the router will not leverage `React.startTransition` on any navigations or state changes. -For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). +For more information, please see the [docs](../../explanation/react-transitions). diff --git a/docs/api/declarative-routers/Router.md b/docs/api/declarative-routers/Router.md index f5a93d50a6..1f4f587dcc 100644 --- a/docs/api/declarative-routers/Router.md +++ b/docs/api/declarative-routers/Router.md @@ -85,5 +85,5 @@ Control whether router state updates are internally wrapped in - When set to `false`, the router will not leverage `React.startTransition` on any navigations or state changes. -For more information, please see the [docs](https://reactrouter.com/explanation/react-transitions). +For more information, please see the [docs](../../explanation/react-transitions). From 362635b8fdf020afdf697823ca71ba4c01b40e0d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 4 May 2026 17:29:58 -0400 Subject: [PATCH 16/23] Move chnageset to change file --- .../react-router/.changes/patch.tough-needles-doubt.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) rename .changeset/tough-needles-doubt.md => packages/react-router/.changes/patch.tough-needles-doubt.md (89%) diff --git a/.changeset/tough-needles-doubt.md b/packages/react-router/.changes/patch.tough-needles-doubt.md similarity index 89% rename from .changeset/tough-needles-doubt.md rename to packages/react-router/.changes/patch.tough-needles-doubt.md index fd0598f784..e8ea03926e 100644 --- a/.changeset/tough-needles-doubt.md +++ b/packages/react-router/.changes/patch.tough-needles-doubt.md @@ -1,8 +1,4 @@ ---- -"react-router": patch ---- - -Improve route matching performance in Framework/Data Mode +Improve route matching performance in Framework/Data Mode ([#14971](https://github.com/remix-run/react-router/pull/14971)) - Avoiding unnecessary calls to `matchRoutes` in data router scenarios - This includes adding back the optimization that was removed in `7.6.0` ([#13562](https://github.com/remix-run/react-router/pull/13562)) From 51a9ccd46effba5a2cfece3618a5e44681bcf58c Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 5 May 2026 09:34:08 -0400 Subject: [PATCH 17/23] Update react router serve docs --- docs/api/other-api/serve.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/docs/api/other-api/serve.md b/docs/api/other-api/serve.md index be5a106b57..ffb940ce8f 100644 --- a/docs/api/other-api/serve.md +++ b/docs/api/other-api/serve.md @@ -14,20 +14,30 @@ You can see the underlying `express` server configuration in [packages/react-rou - [`express.static`][express-static] (and thus [`serve-static`][serve-static]) - [`morgan`][morgan] -## `HOST` environment variable +## Usage -You can configure the hostname for your Express app via `process.env.HOST` and that value will be passed to the internal [`app.listen`][express-listen] method when starting the server. +Install `@react-router/serve`: -```shellscript nonumber -HOST=127.0.0.1 npx react-router-serve build/index.js +```sh nonumber +npm install @react-router/serve ``` -```shellscript nonumber +Run the server with your server build: + +```sh nonumber react-router-serve # e.g. react-router-serve build/index.js ``` +## `HOST` environment variable + +You can configure the hostname for your Express app via `process.env.HOST` and that value will be passed to the internal [`app.listen`][express-listen] method when starting the server. + +```shellscript nonumber +HOST=127.0.0.1 npx react-router-serve build/index.js +``` + ## `PORT` environment variable You can change the port of the server with an environment variable. From 98692080aabe831ff3bf0a3e45ec482adea25419 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 5 May 2026 09:48:08 -0400 Subject: [PATCH 18/23] More docs updates --- docs/api/other-api/serve.md | 2 +- docs/tutorials/quickstart.md | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/api/other-api/serve.md b/docs/api/other-api/serve.md index ffb940ce8f..460a7fcb6b 100644 --- a/docs/api/other-api/serve.md +++ b/docs/api/other-api/serve.md @@ -54,7 +54,7 @@ The `server-build-path` needs to point to the `serverBuildPath` defined in [`rea Because only the build artifacts (`build/`, `public/build/`) need to be deployed to production, the `react-router.config.ts` is not guaranteed to be available in production, so you need to tell React Router where your server build is with this option. -In development, `react-router-serve` will ensure the latest code is run by purging the `require` cache for every request. This has some effects on your code you might need to be aware of: +In development, `@react-router/serve` will ensure the latest code is run by purging the `require` cache for every request. This has some effects on your code you might need to be aware of: - Any values in the module scope will be "reset" diff --git a/docs/tutorials/quickstart.md b/docs/tutorials/quickstart.md index 915e27086a..3073765994 100644 --- a/docs/tutorials/quickstart.md +++ b/docs/tutorials/quickstart.md @@ -114,7 +114,7 @@ You should now see a `build` folder containing a `server` folder (the server ver 👉 **Run the app with `react-router-serve`** -Now you can run your app with `react-router-serve`: +Now you can run your app with `react-router-serve`, provided by the `@react-router/serve` package: ```shellscript nonumber npx react-router-serve build/server/index.js @@ -142,9 +142,9 @@ You can also use React Router as a Single Page Application with no server. For m -If you don't care to set up your own server, you can use `react-router-serve`. It's a simple `express`-based server maintained by the React Router maintainers. However, React Router is specifically designed to run in _any_ JavaScript environment so that you own your stack. It is expected many —if not most— production apps will have their own server. +If you don't care to set up your own server, you can use `@react-router/serve`. It's a simple `express`-based server maintained by the React Router maintainers. However, React Router is specifically designed to run in _any_ JavaScript environment so that you own your stack. It is expected many —if not most— production apps will have their own server. -Just for kicks, let's stop using `react-router-serve` and use `express` instead. +Just for kicks, let's stop using `@react-router/serve` and use `express` instead. 👉 **Install Express, the React Router Express adapter, and [cross-env] for running in production mode** From a993f09533fa15bbf01ce734c8c7c116564cf4b7 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 5 May 2026 10:25:17 -0400 Subject: [PATCH 19/23] Update change files --- .../.changes/minor.stabilize-pass-through-requests.md | 2 ++ .../.changes/minor.stabilize-prerender-concurrency.md | 2 ++ packages/react-router-dev/.changes/minor.stabilize-sri.md | 2 ++ .../.changes/minor.stabilize-default-should-revalidate.md | 2 ++ .../react-router/.changes/minor.stabilize-instrumentations.md | 1 + packages/react-router/.changes/minor.stabilize-mask.md | 2 ++ .../react-router/.changes/minor.stabilize-normalize-path.md | 2 ++ .../.changes/minor.stabilize-pass-through-requests.md | 2 ++ packages/react-router/.changes/minor.stabilize-sri.md | 2 ++ packages/react-router/.changes/minor.stabilize-url.md | 2 ++ .../react-router/.changes/minor.stabilize-use-transitions.md | 2 ++ 11 files changed, 21 insertions(+) diff --git a/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md b/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md index fa624d1d0f..71700d0ab3 100644 --- a/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md +++ b/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md @@ -1 +1,3 @@ Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md b/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md index 8ce1239d9d..f22212cf5b 100644 --- a/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md +++ b/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md @@ -1 +1,3 @@ Stabilize `prerender.unstable_concurrency` as `prerender.concurrency` + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router-dev/.changes/minor.stabilize-sri.md b/packages/react-router-dev/.changes/minor.stabilize-sri.md index aec5614143..d10cca9241 100644 --- a/packages/react-router-dev/.changes/minor.stabilize-sri.md +++ b/packages/react-router-dev/.changes/minor.stabilize-sri.md @@ -1 +1,3 @@ Stabilize `future.unstable_subResourceIntegrity` as a top-level `subResourceIntegrity` config option in `react-router.config.ts` + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md b/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md index 4c44d53229..2d75ac3642 100644 --- a/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md +++ b/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md @@ -1 +1,3 @@ Stabilize `unstable_defaultShouldRevalidate` as `defaultShouldRevalidate` on ``, ``, `useLinkClickHandler`, `useSubmit`, `fetcher.submit`, and `setSearchParams` + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-instrumentations.md b/packages/react-router/.changes/minor.stabilize-instrumentations.md index ece1fa949f..3c951e992c 100644 --- a/packages/react-router/.changes/minor.stabilize-instrumentations.md +++ b/packages/react-router/.changes/minor.stabilize-instrumentations.md @@ -1,3 +1,4 @@ Stabilize the instrumentation APIs. `unstable_instrumentations` is now `instrumentations` and `unstable_pattern` is now `pattern` - The `unstable_ServerInstrumentation`, `unstable_ClientInstrumentation`, `unstable_InstrumentRequestHandlerFunction`, `unstable_InstrumentRouterFunction`, `unstable_InstrumentRouteFunction`, and `unstable_InstrumentationHandlerResult` types have had their `unstable_` prefixes removed +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-mask.md b/packages/react-router/.changes/minor.stabilize-mask.md index 0e9663fe0c..1e5a57bfcf 100644 --- a/packages/react-router/.changes/minor.stabilize-mask.md +++ b/packages/react-router/.changes/minor.stabilize-mask.md @@ -1 +1,3 @@ Stabilize `unstable_mask` as `mask` on ``, `useLinkClickHandler`, and `useNavigate`, and rename the corresponding `Location.unstable_mask` field to `Location.mask` + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-normalize-path.md b/packages/react-router/.changes/minor.stabilize-normalize-path.md index f9fcc61ec1..76f6c2728f 100644 --- a/packages/react-router/.changes/minor.stabilize-normalize-path.md +++ b/packages/react-router/.changes/minor.stabilize-normalize-path.md @@ -1 +1,3 @@ Stabilize the `unstable_normalizePath` option on `staticHandler.query` and `staticHandler.queryRoute` as `normalizePath` + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-pass-through-requests.md b/packages/react-router/.changes/minor.stabilize-pass-through-requests.md index fa624d1d0f..71700d0ab3 100644 --- a/packages/react-router/.changes/minor.stabilize-pass-through-requests.md +++ b/packages/react-router/.changes/minor.stabilize-pass-through-requests.md @@ -1 +1,3 @@ Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-sri.md b/packages/react-router/.changes/minor.stabilize-sri.md index 9fef86d380..85952d8bce 100644 --- a/packages/react-router/.changes/minor.stabilize-sri.md +++ b/packages/react-router/.changes/minor.stabilize-sri.md @@ -1 +1,3 @@ Remove `unstable_subResourceIntegrity` from the runtime `FutureConfig` type; the flag is now controlled by the top-level `subResourceIntegrity` option in `react-router.config.ts` + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-url.md b/packages/react-router/.changes/minor.stabilize-url.md index e14bee8989..cc171597a0 100644 --- a/packages/react-router/.changes/minor.stabilize-url.md +++ b/packages/react-router/.changes/minor.stabilize-url.md @@ -1 +1,3 @@ Stabilize `unstable_url` as `url` on `loader`, `action`, and `middleware` function args + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-use-transitions.md b/packages/react-router/.changes/minor.stabilize-use-transitions.md index 38b60ffd8c..44dd4f9dc9 100644 --- a/packages/react-router/.changes/minor.stabilize-use-transitions.md +++ b/packages/react-router/.changes/minor.stabilize-use-transitions.md @@ -1 +1,3 @@ Stabilize `unstable_useTransitions` as `useTransitions` on ``, ``, ``, ``, ``, ``, ``, and `useLinkClickHandler` + +- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly From af5d49b71c15fa502cb0918482597284e8cb39c4 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 5 May 2026 10:28:54 -0400 Subject: [PATCH 20/23] Update change files again --- .../patch.add-nonce-scripts-modulepreload.md | 2 +- .../.changes/patch.hot-bottles-mix.md | 19 +------------------ .../.changes/patch.tough-needles-doubt.md | 18 +----------------- 3 files changed, 3 insertions(+), 36 deletions(-) diff --git a/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md b/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md index ba4ab6112e..dfaae93ebc 100644 --- a/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md +++ b/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md @@ -1 +1 @@ -Add nonce to scripts modulepreload +Add `nonce` to `` `` elements (if provided) diff --git a/packages/react-router/.changes/patch.hot-bottles-mix.md b/packages/react-router/.changes/patch.hot-bottles-mix.md index 7c9a62a357..6d57942ed9 100644 --- a/packages/react-router/.changes/patch.hot-bottles-mix.md +++ b/packages/react-router/.changes/patch.hot-bottles-mix.md @@ -1,20 +1,3 @@ Improve server-side route matching performance by pre-computing flattened/cached route branches ([#14967](https://github.com/remix-run/react-router/pull/14967)) -- Performance benchmark - - 100 route app, 3000 requests @ 10x concurrency across 38 distinct paths - - `dev` branch - - Throughput: 826.1 req/s - - Latency mean: 10.227ms - - Latency p50: 11.125ms - - Latency p95: 13.056ms - - Latency p99: 16.753ms - - Latency min: 1.739ms - - Latency max: 20.268ms - - This branch - - Throughput: 952.7 req/s (15.3% improvement) - - Latency mean: 8.716ms (14.8% improvement) - - Latency p50: 9.452ms - - Latency p95: 11.610ms - - Latency p99: 12.544ms - - Latency min: 1.656ms - - Latency max: 15.936ms +- Performance benchmarks showed roughly a 10-15% improvement in server-side request handling performance diff --git a/packages/react-router/.changes/patch.tough-needles-doubt.md b/packages/react-router/.changes/patch.tough-needles-doubt.md index e8ea03926e..12603c46c6 100644 --- a/packages/react-router/.changes/patch.tough-needles-doubt.md +++ b/packages/react-router/.changes/patch.tough-needles-doubt.md @@ -4,20 +4,4 @@ Improve route matching performance in Framework/Data Mode ([#14971](https://gith - This includes adding back the optimization that was removed in `7.6.0` ([#13562](https://github.com/remix-run/react-router/pull/13562)) - The issues that prompted the revert have been addressed by using the available router `matches` but always updating `match.route` to the latest route in the `manifest` - Leverage pre-computed pre-computing flattened/cached route branches during client side route matching -- This builds on top of prior server optimizations and provides an additional set of gains (~30%): - - Original server optimizations branch - - Throughput: 952.7 req/s - - Latency mean: 8.716ms - - Latency p50: 9.452ms - - Latency p95: 11.610ms - - Latency p99: 12.544ms - - Latency min: 1.656ms - - Latency max: 15.936ms - - This branch - - Throughput: 1235.3 req/s - - Latency mean: 6.095ms - - Latency p50: 6.655ms - - Latency p95: 8.327ms - - Latency p99: 12.133ms - - Latency min: 1.066ms - - Latency max: 19.056ms +- Performance benchmarks showed roughly a 15-30% improvement in server-side request handling performance From 97c8de79c38f107acd15f74d8295c7bf75894a5d Mon Sep 17 00:00:00 2001 From: Ryan Florence Date: Tue, 5 May 2026 08:30:49 -0600 Subject: [PATCH 21/23] Release v7.15.0 (#15018) Co-authored-by: Remix Run Bot --- CHANGELOG.md | 76 +++++++++++++++++++ packages/create-react-router/CHANGELOG.md | 6 ++ packages/create-react-router/package.json | 2 +- packages/react-router-architect/CHANGELOG.md | 8 ++ packages/react-router-architect/package.json | 2 +- packages/react-router-cloudflare/CHANGELOG.md | 7 ++ packages/react-router-cloudflare/package.json | 2 +- .../minor.stabilize-pass-through-requests.md | 3 - .../minor.stabilize-prerender-concurrency.md | 3 - .../.changes/minor.stabilize-sri.md | 3 - packages/react-router-dev/CHANGELOG.md | 23 ++++++ packages/react-router-dev/package.json | 2 +- packages/react-router-dom/CHANGELOG.md | 7 ++ packages/react-router-dom/package.json | 2 +- packages/react-router-express/CHANGELOG.md | 8 ++ packages/react-router-express/package.json | 2 +- packages/react-router-fs-routes/CHANGELOG.md | 7 ++ packages/react-router-fs-routes/package.json | 2 +- packages/react-router-node/CHANGELOG.md | 7 ++ packages/react-router-node/package.json | 2 +- .../CHANGELOG.md | 7 ++ .../package.json | 2 +- packages/react-router-serve/CHANGELOG.md | 9 +++ packages/react-router-serve/package.json | 2 +- ...nor.stabilize-default-should-revalidate.md | 3 - .../minor.stabilize-instrumentations.md | 4 - .../.changes/minor.stabilize-mask.md | 3 - .../minor.stabilize-normalize-path.md | 3 - .../minor.stabilize-pass-through-requests.md | 3 - .../.changes/minor.stabilize-sri.md | 3 - .../.changes/minor.stabilize-url.md | 3 - .../minor.stabilize-use-transitions.md | 3 - .../patch.add-nonce-scripts-modulepreload.md | 1 - ...faultshouldrevalidatefalse-used-matched.md | 1 - .../.changes/patch.hot-bottles-mix.md | 3 - .../patch.made-mask-optional-in-location.md | 1 - ...-toute-matching-caching-flattenedranked.md | 1 - .../.changes/patch.tough-needles-doubt.md | 7 -- packages/react-router/CHANGELOG.md | 59 ++++++++++++++ packages/react-router/package.json | 2 +- 40 files changed, 235 insertions(+), 59 deletions(-) delete mode 100644 packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md delete mode 100644 packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md delete mode 100644 packages/react-router-dev/.changes/minor.stabilize-sri.md delete mode 100644 packages/react-router/.changes/minor.stabilize-default-should-revalidate.md delete mode 100644 packages/react-router/.changes/minor.stabilize-instrumentations.md delete mode 100644 packages/react-router/.changes/minor.stabilize-mask.md delete mode 100644 packages/react-router/.changes/minor.stabilize-normalize-path.md delete mode 100644 packages/react-router/.changes/minor.stabilize-pass-through-requests.md delete mode 100644 packages/react-router/.changes/minor.stabilize-sri.md delete mode 100644 packages/react-router/.changes/minor.stabilize-url.md delete mode 100644 packages/react-router/.changes/minor.stabilize-use-transitions.md delete mode 100644 packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md delete mode 100644 packages/react-router/.changes/patch.fixes-bug-when-unstabledefaultshouldrevalidatefalse-used-matched.md delete mode 100644 packages/react-router/.changes/patch.hot-bottles-mix.md delete mode 100644 packages/react-router/.changes/patch.made-mask-optional-in-location.md delete mode 100644 packages/react-router/.changes/patch.optimize-serverside-toute-matching-caching-flattenedranked.md delete mode 100644 packages/react-router/.changes/patch.tough-needles-doubt.md diff --git a/CHANGELOG.md b/CHANGELOG.md index d467b70771..39610f1442 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ We manage release notes in this file instead of the paginated Github Releases Pa Table of Contents - [React Router Releases](#react-router-releases) + - [v7.15.0](#v7150) - [v7.14.2](#v7142) - [v7.14.1](#v7141) - [v7.14.0](#v7140) @@ -169,6 +170,81 @@ We manage release notes in this file instead of the paginated Github Releases Pa +## v7.15.0 + +Date: 2026-05-05 + +### Minor Changes + +- `react-router` - Stabilize `unstable_defaultShouldRevalidate` as `defaultShouldRevalidate` on ``, ``, `useLinkClickHandler`, `useSubmit`, `fetcher.submit`, and `setSearchParams` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `react-router` - Stabilize the instrumentation APIs. `unstable_instrumentations` is now `instrumentations` and `unstable_pattern` is now `pattern` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - The `unstable_ServerInstrumentation`, `unstable_ClientInstrumentation`, `unstable_InstrumentRequestHandlerFunction`, `unstable_InstrumentRouterFunction`, `unstable_InstrumentRouteFunction`, and `unstable_InstrumentationHandlerResult` types have had their `unstable_` prefixes removed + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `react-router` - Stabilize `unstable_mask` as `mask` on ``, `useLinkClickHandler`, and `useNavigate`, and rename the corresponding `Location.unstable_mask` field to `Location.mask` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `react-router` - Stabilize the `unstable_normalizePath` option on `staticHandler.query` and `staticHandler.queryRoute` as `normalizePath` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `react-router` - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `react-router` - Remove `unstable_subResourceIntegrity` from the runtime `FutureConfig` type; the flag is now controlled by the top-level `subResourceIntegrity` option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `react-router` - Stabilize `unstable_url` as `url` on `loader`, `action`, and `middleware` function args ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `react-router` - Stabilize `unstable_useTransitions` as `useTransitions` on ``, ``, ``, ``, ``, ``, ``, and `useLinkClickHandler` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `@react-router/dev` - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `@react-router/dev` - Stabilize `prerender.unstable_concurrency` as `prerender.concurrency` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- `@react-router/dev` - Stabilize `future.unstable_subResourceIntegrity` as a top-level `subResourceIntegrity` config option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +### Patch Changes + +- `react-router` - Add `nonce` to `` `` elements (if provided) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) + +- `react-router` - Fix a bug with `unstable_defaultShouldRevalidate={false}` where parent routes that did not export a `shouldRevalidate` function could be incorrectly included in the single fetch call for new child route data ([#15012](https://github.com/remix-run/react-router/pull/15012)) + +- `react-router` - Improve server-side route matching performance by pre-computing flattened/cached route branches ([#14967](https://github.com/remix-run/react-router/pull/14967)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) + + - Performance benchmarks showed roughly a 10-15% improvement in server-side request handling performance + +- `react-router` - Mark `mask` as an optional field in `Location` for easier mocking in unit tests ([#14999](https://github.com/remix-run/react-router/pull/14999)) + +- `react-router` - Cache flattened/ranked route branches to optimize server-side route matching ([#14967](https://github.com/remix-run/react-router/pull/14967)) + +- `react-router` - Improve route matching performance in Framework/Data Mode ([#14971](https://github.com/remix-run/react-router/pull/14971)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) + + - Avoiding unnecessary calls to `matchRoutes` in data router scenarios + - This includes adding back the optimization that was removed in `7.6.0` ([#13562](https://github.com/remix-run/react-router/pull/13562)) + - The issues that prompted the revert have been addressed by using the available router `matches` but always updating `match.route` to the latest route in the `manifest` + - Leverage pre-computed pre-computing flattened/cached route branches during client side route matching + - Performance benchmarks showed roughly a 15-30% improvement in server-side request handling performance + +**Full Changelog**: [`v7.14.2...v7.15.0`](https://github.com/remix-run/react-router/compare/react-router@7.14.2...react-router@7.15.0) + ## v7.14.2 Date: 2026-04-21 diff --git a/packages/create-react-router/CHANGELOG.md b/packages/create-react-router/CHANGELOG.md index 3a28edf758..2648035946 100644 --- a/packages/create-react-router/CHANGELOG.md +++ b/packages/create-react-router/CHANGELOG.md @@ -1,5 +1,11 @@ # `create-react-router` +## v7.15.0 + +### Patch Changes + +- _No changes_ + ## v7.14.2 ### Patch Changes diff --git a/packages/create-react-router/package.json b/packages/create-react-router/package.json index c05ba22ba3..c713d903ec 100644 --- a/packages/create-react-router/package.json +++ b/packages/create-react-router/package.json @@ -1,6 +1,6 @@ { "name": "create-react-router", - "version": "7.14.2", + "version": "7.15.0", "description": "Create a new React Router app", "homepage": "https://reactrouter.com", "bugs": { diff --git a/packages/react-router-architect/CHANGELOG.md b/packages/react-router-architect/CHANGELOG.md index d16b8c1782..bc868a16df 100644 --- a/packages/react-router-architect/CHANGELOG.md +++ b/packages/react-router-architect/CHANGELOG.md @@ -1,5 +1,13 @@ # `@react-router/architect` +## v7.15.0 + +### Patch Changes + +- Updated dependencies: + - [`react-router@7.15.0`](https://github.com/remix-run/react-router/releases/tag/react-router@7.15.0) + - [`@react-router/node@7.15.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/node@7.15.0) + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router-architect/package.json b/packages/react-router-architect/package.json index 60aee5f3e3..5b03818dfb 100644 --- a/packages/react-router-architect/package.json +++ b/packages/react-router-architect/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/architect", - "version": "7.14.2", + "version": "7.15.0", "description": "Architect server request handler for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-cloudflare/CHANGELOG.md b/packages/react-router-cloudflare/CHANGELOG.md index f7cf5ab018..6bc00b9a99 100644 --- a/packages/react-router-cloudflare/CHANGELOG.md +++ b/packages/react-router-cloudflare/CHANGELOG.md @@ -1,5 +1,12 @@ # `@react-router/cloudflare` +## v7.15.0 + +### Patch Changes + +- Updated dependencies: + - [`react-router@7.15.0`](https://github.com/remix-run/react-router/releases/tag/react-router@7.15.0) + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router-cloudflare/package.json b/packages/react-router-cloudflare/package.json index 6087fc99d0..0bcfa7ff1a 100644 --- a/packages/react-router-cloudflare/package.json +++ b/packages/react-router-cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/cloudflare", - "version": "7.14.2", + "version": "7.15.0", "description": "Cloudflare platform abstractions for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md b/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md deleted file mode 100644 index 71700d0ab3..0000000000 --- a/packages/react-router-dev/.changes/minor.stabilize-pass-through-requests.md +++ /dev/null @@ -1,3 +0,0 @@ -Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md b/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md deleted file mode 100644 index f22212cf5b..0000000000 --- a/packages/react-router-dev/.changes/minor.stabilize-prerender-concurrency.md +++ /dev/null @@ -1,3 +0,0 @@ -Stabilize `prerender.unstable_concurrency` as `prerender.concurrency` - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router-dev/.changes/minor.stabilize-sri.md b/packages/react-router-dev/.changes/minor.stabilize-sri.md deleted file mode 100644 index d10cca9241..0000000000 --- a/packages/react-router-dev/.changes/minor.stabilize-sri.md +++ /dev/null @@ -1,3 +0,0 @@ -Stabilize `future.unstable_subResourceIntegrity` as a top-level `subResourceIntegrity` config option in `react-router.config.ts` - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router-dev/CHANGELOG.md b/packages/react-router-dev/CHANGELOG.md index 4830dafa98..894a6c44cd 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -1,5 +1,28 @@ # `@react-router/dev` +## v7.15.0 + +### Minor Changes + +- Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- Stabilize `prerender.unstable_concurrency` as `prerender.concurrency` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- Stabilize `future.unstable_subResourceIntegrity` as a top-level `subResourceIntegrity` config option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +### Patch Changes + +- Updated dependencies: + - [`react-router@7.15.0`](https://github.com/remix-run/react-router/releases/tag/react-router@7.15.0) + - [`@react-router/node@7.15.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/node@7.15.0) + - [`@react-router/serve@7.15.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/serve@7.15.0) + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router-dev/package.json b/packages/react-router-dev/package.json index 834b4930fa..61e32c46a3 100644 --- a/packages/react-router-dev/package.json +++ b/packages/react-router-dev/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/dev", - "version": "7.14.2", + "version": "7.15.0", "description": "Dev tools and CLI for React Router", "homepage": "https://reactrouter.com", "bugs": { diff --git a/packages/react-router-dom/CHANGELOG.md b/packages/react-router-dom/CHANGELOG.md index 2fd41ac56c..8568eb6efb 100644 --- a/packages/react-router-dom/CHANGELOG.md +++ b/packages/react-router-dom/CHANGELOG.md @@ -1,5 +1,12 @@ # react-router-dom +## v7.15.0 + +### Patch Changes + +- Updated dependencies: + - [`react-router@7.15.0`](https://github.com/remix-run/react-router/releases/tag/react-router@7.15.0) + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router-dom/package.json b/packages/react-router-dom/package.json index a843ed6dca..20bcdf6209 100644 --- a/packages/react-router-dom/package.json +++ b/packages/react-router-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-router-dom", - "version": "7.14.2", + "version": "7.15.0", "description": "Declarative routing for React web applications", "keywords": [ "react", diff --git a/packages/react-router-express/CHANGELOG.md b/packages/react-router-express/CHANGELOG.md index bf63dc77a5..3e3bd778b4 100644 --- a/packages/react-router-express/CHANGELOG.md +++ b/packages/react-router-express/CHANGELOG.md @@ -1,5 +1,13 @@ # `@react-router/express` +## v7.15.0 + +### Patch Changes + +- Updated dependencies: + - [`react-router@7.15.0`](https://github.com/remix-run/react-router/releases/tag/react-router@7.15.0) + - [`@react-router/node@7.15.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/node@7.15.0) + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router-express/package.json b/packages/react-router-express/package.json index 24175906b3..113b274acb 100644 --- a/packages/react-router-express/package.json +++ b/packages/react-router-express/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/express", - "version": "7.14.2", + "version": "7.15.0", "description": "Express server request handler for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-fs-routes/CHANGELOG.md b/packages/react-router-fs-routes/CHANGELOG.md index 290f3b1c80..342d494f70 100644 --- a/packages/react-router-fs-routes/CHANGELOG.md +++ b/packages/react-router-fs-routes/CHANGELOG.md @@ -1,5 +1,12 @@ # `@react-router/fs-routes` +## v7.15.0 + +### Patch Changes + +- Updated dependencies: + - [`@react-router/dev@7.15.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/dev@7.15.0) + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router-fs-routes/package.json b/packages/react-router-fs-routes/package.json index e217b23532..328242c6bc 100644 --- a/packages/react-router-fs-routes/package.json +++ b/packages/react-router-fs-routes/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/fs-routes", - "version": "7.14.2", + "version": "7.15.0", "description": "File system routing conventions for React Router, for use within routes.ts", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-node/CHANGELOG.md b/packages/react-router-node/CHANGELOG.md index 4c3a010e1e..8829857f33 100644 --- a/packages/react-router-node/CHANGELOG.md +++ b/packages/react-router-node/CHANGELOG.md @@ -1,5 +1,12 @@ # `@react-router/node` +## v7.15.0 + +### Patch Changes + +- Updated dependencies: + - [`react-router@7.15.0`](https://github.com/remix-run/react-router/releases/tag/react-router@7.15.0) + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router-node/package.json b/packages/react-router-node/package.json index 3bd2dc78c6..9e2a4a9bd2 100644 --- a/packages/react-router-node/package.json +++ b/packages/react-router-node/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/node", - "version": "7.14.2", + "version": "7.15.0", "description": "Node.js platform abstractions for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-remix-routes-option-adapter/CHANGELOG.md b/packages/react-router-remix-routes-option-adapter/CHANGELOG.md index a5f649f56f..8d36d19d9b 100644 --- a/packages/react-router-remix-routes-option-adapter/CHANGELOG.md +++ b/packages/react-router-remix-routes-option-adapter/CHANGELOG.md @@ -1,5 +1,12 @@ # `@react-router/remix-config-routes-adapter` +## v7.15.0 + +### Patch Changes + +- Updated dependencies: + - [`@react-router/dev@7.15.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/dev@7.15.0) + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router-remix-routes-option-adapter/package.json b/packages/react-router-remix-routes-option-adapter/package.json index 59338ddeb4..a53efa460a 100644 --- a/packages/react-router-remix-routes-option-adapter/package.json +++ b/packages/react-router-remix-routes-option-adapter/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/remix-routes-option-adapter", - "version": "7.14.2", + "version": "7.15.0", "description": "Adapter for Remix's \"routes\" config option, for use within routes.ts", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router-serve/CHANGELOG.md b/packages/react-router-serve/CHANGELOG.md index dca4637576..64d2fd37e6 100644 --- a/packages/react-router-serve/CHANGELOG.md +++ b/packages/react-router-serve/CHANGELOG.md @@ -1,5 +1,14 @@ # `@react-router/serve` +## v7.15.0 + +### Patch Changes + +- Updated dependencies: + - [`react-router@7.15.0`](https://github.com/remix-run/react-router/releases/tag/react-router@7.15.0) + - [`@react-router/express@7.15.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/express@7.15.0) + - [`@react-router/node@7.15.0`](https://github.com/remix-run/react-router/releases/tag/@react-router/node@7.15.0) + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router-serve/package.json b/packages/react-router-serve/package.json index 047536e71e..3737445020 100644 --- a/packages/react-router-serve/package.json +++ b/packages/react-router-serve/package.json @@ -1,6 +1,6 @@ { "name": "@react-router/serve", - "version": "7.14.2", + "version": "7.15.0", "description": "Production application server for React Router", "bugs": { "url": "https://github.com/remix-run/react-router/issues" diff --git a/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md b/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md deleted file mode 100644 index 2d75ac3642..0000000000 --- a/packages/react-router/.changes/minor.stabilize-default-should-revalidate.md +++ /dev/null @@ -1,3 +0,0 @@ -Stabilize `unstable_defaultShouldRevalidate` as `defaultShouldRevalidate` on ``, ``, `useLinkClickHandler`, `useSubmit`, `fetcher.submit`, and `setSearchParams` - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-instrumentations.md b/packages/react-router/.changes/minor.stabilize-instrumentations.md deleted file mode 100644 index 3c951e992c..0000000000 --- a/packages/react-router/.changes/minor.stabilize-instrumentations.md +++ /dev/null @@ -1,4 +0,0 @@ -Stabilize the instrumentation APIs. `unstable_instrumentations` is now `instrumentations` and `unstable_pattern` is now `pattern` - -- The `unstable_ServerInstrumentation`, `unstable_ClientInstrumentation`, `unstable_InstrumentRequestHandlerFunction`, `unstable_InstrumentRouterFunction`, `unstable_InstrumentRouteFunction`, and `unstable_InstrumentationHandlerResult` types have had their `unstable_` prefixes removed -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-mask.md b/packages/react-router/.changes/minor.stabilize-mask.md deleted file mode 100644 index 1e5a57bfcf..0000000000 --- a/packages/react-router/.changes/minor.stabilize-mask.md +++ /dev/null @@ -1,3 +0,0 @@ -Stabilize `unstable_mask` as `mask` on ``, `useLinkClickHandler`, and `useNavigate`, and rename the corresponding `Location.unstable_mask` field to `Location.mask` - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-normalize-path.md b/packages/react-router/.changes/minor.stabilize-normalize-path.md deleted file mode 100644 index 76f6c2728f..0000000000 --- a/packages/react-router/.changes/minor.stabilize-normalize-path.md +++ /dev/null @@ -1,3 +0,0 @@ -Stabilize the `unstable_normalizePath` option on `staticHandler.query` and `staticHandler.queryRoute` as `normalizePath` - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-pass-through-requests.md b/packages/react-router/.changes/minor.stabilize-pass-through-requests.md deleted file mode 100644 index 71700d0ab3..0000000000 --- a/packages/react-router/.changes/minor.stabilize-pass-through-requests.md +++ /dev/null @@ -1,3 +0,0 @@ -Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-sri.md b/packages/react-router/.changes/minor.stabilize-sri.md deleted file mode 100644 index 85952d8bce..0000000000 --- a/packages/react-router/.changes/minor.stabilize-sri.md +++ /dev/null @@ -1,3 +0,0 @@ -Remove `unstable_subResourceIntegrity` from the runtime `FutureConfig` type; the flag is now controlled by the top-level `subResourceIntegrity` option in `react-router.config.ts` - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-url.md b/packages/react-router/.changes/minor.stabilize-url.md deleted file mode 100644 index cc171597a0..0000000000 --- a/packages/react-router/.changes/minor.stabilize-url.md +++ /dev/null @@ -1,3 +0,0 @@ -Stabilize `unstable_url` as `url` on `loader`, `action`, and `middleware` function args - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/minor.stabilize-use-transitions.md b/packages/react-router/.changes/minor.stabilize-use-transitions.md deleted file mode 100644 index 44dd4f9dc9..0000000000 --- a/packages/react-router/.changes/minor.stabilize-use-transitions.md +++ /dev/null @@ -1,3 +0,0 @@ -Stabilize `unstable_useTransitions` as `useTransitions` on ``, ``, ``, ``, ``, ``, ``, and `useLinkClickHandler` - -- ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly diff --git a/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md b/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md deleted file mode 100644 index dfaae93ebc..0000000000 --- a/packages/react-router/.changes/patch.add-nonce-scripts-modulepreload.md +++ /dev/null @@ -1 +0,0 @@ -Add `nonce` to `` `` elements (if provided) diff --git a/packages/react-router/.changes/patch.fixes-bug-when-unstabledefaultshouldrevalidatefalse-used-matched.md b/packages/react-router/.changes/patch.fixes-bug-when-unstabledefaultshouldrevalidatefalse-used-matched.md deleted file mode 100644 index d951f77691..0000000000 --- a/packages/react-router/.changes/patch.fixes-bug-when-unstabledefaultshouldrevalidatefalse-used-matched.md +++ /dev/null @@ -1 +0,0 @@ -Fix a bug with `unstable_defaultShouldRevalidate={false}` where parent routes that did not export a `shouldRevalidate` function could be incorrectly included in the single fetch call for new child route data diff --git a/packages/react-router/.changes/patch.hot-bottles-mix.md b/packages/react-router/.changes/patch.hot-bottles-mix.md deleted file mode 100644 index 6d57942ed9..0000000000 --- a/packages/react-router/.changes/patch.hot-bottles-mix.md +++ /dev/null @@ -1,3 +0,0 @@ -Improve server-side route matching performance by pre-computing flattened/cached route branches ([#14967](https://github.com/remix-run/react-router/pull/14967)) - -- Performance benchmarks showed roughly a 10-15% improvement in server-side request handling performance diff --git a/packages/react-router/.changes/patch.made-mask-optional-in-location.md b/packages/react-router/.changes/patch.made-mask-optional-in-location.md deleted file mode 100644 index b2a92ebcf1..0000000000 --- a/packages/react-router/.changes/patch.made-mask-optional-in-location.md +++ /dev/null @@ -1 +0,0 @@ -Mark `mask` as an optional field in `Location` for easier mocking in unit tests diff --git a/packages/react-router/.changes/patch.optimize-serverside-toute-matching-caching-flattenedranked.md b/packages/react-router/.changes/patch.optimize-serverside-toute-matching-caching-flattenedranked.md deleted file mode 100644 index 7ca17d4b24..0000000000 --- a/packages/react-router/.changes/patch.optimize-serverside-toute-matching-caching-flattenedranked.md +++ /dev/null @@ -1 +0,0 @@ -Cache flattened/ranked route branches to optimize server-side route matching diff --git a/packages/react-router/.changes/patch.tough-needles-doubt.md b/packages/react-router/.changes/patch.tough-needles-doubt.md deleted file mode 100644 index 12603c46c6..0000000000 --- a/packages/react-router/.changes/patch.tough-needles-doubt.md +++ /dev/null @@ -1,7 +0,0 @@ -Improve route matching performance in Framework/Data Mode ([#14971](https://github.com/remix-run/react-router/pull/14971)) - -- Avoiding unnecessary calls to `matchRoutes` in data router scenarios - - This includes adding back the optimization that was removed in `7.6.0` ([#13562](https://github.com/remix-run/react-router/pull/13562)) - - The issues that prompted the revert have been addressed by using the available router `matches` but always updating `match.route` to the latest route in the `manifest` -- Leverage pre-computed pre-computing flattened/cached route branches during client side route matching -- Performance benchmarks showed roughly a 15-30% improvement in server-side request handling performance diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index b22aeb4e9e..d0cee515ba 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -1,5 +1,64 @@ # `react-router` +## v7.15.0 + +### Minor Changes + +- Stabilize `unstable_defaultShouldRevalidate` as `defaultShouldRevalidate` on ``, ``, `useLinkClickHandler`, `useSubmit`, `fetcher.submit`, and `setSearchParams` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- Stabilize the instrumentation APIs. `unstable_instrumentations` is now `instrumentations` and `unstable_pattern` is now `pattern` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - The `unstable_ServerInstrumentation`, `unstable_ClientInstrumentation`, `unstable_InstrumentRequestHandlerFunction`, `unstable_InstrumentRouterFunction`, `unstable_InstrumentRouteFunction`, and `unstable_InstrumentationHandlerResult` types have had their `unstable_` prefixes removed + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- Stabilize `unstable_mask` as `mask` on ``, `useLinkClickHandler`, and `useNavigate`, and rename the corresponding `Location.unstable_mask` field to `Location.mask` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- Stabilize the `unstable_normalizePath` option on `staticHandler.query` and `staticHandler.queryRoute` as `normalizePath` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- Remove `unstable_subResourceIntegrity` from the runtime `FutureConfig` type; the flag is now controlled by the top-level `subResourceIntegrity` option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- Stabilize `unstable_url` as `url` on `loader`, `action`, and `middleware` function args ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +- Stabilize `unstable_useTransitions` as `useTransitions` on ``, ``, ``, ``, ``, ``, ``, and `useLinkClickHandler` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) + + - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly + +### Patch Changes + +- Add `nonce` to `` `` elements (if provided) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) + +- Fix a bug with `unstable_defaultShouldRevalidate={false}` where parent routes that did not export a `shouldRevalidate` function could be incorrectly included in the single fetch call for new child route data ([#15012](https://github.com/remix-run/react-router/pull/15012)) + +- Improve server-side route matching performance by pre-computing flattened/cached route branches ([#14967](https://github.com/remix-run/react-router/pull/14967)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) + + - Performance benchmarks showed roughly a 10-15% improvement in server-side request handling performance + +- Mark `mask` as an optional field in `Location` for easier mocking in unit tests ([#14999](https://github.com/remix-run/react-router/pull/14999)) + +- Cache flattened/ranked route branches to optimize server-side route matching ([#14967](https://github.com/remix-run/react-router/pull/14967)) + +- Improve route matching performance in Framework/Data Mode ([#14971](https://github.com/remix-run/react-router/pull/14971)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) + + - Avoiding unnecessary calls to `matchRoutes` in data router scenarios + - This includes adding back the optimization that was removed in `7.6.0` ([#13562](https://github.com/remix-run/react-router/pull/13562)) + - The issues that prompted the revert have been addressed by using the available router `matches` but always updating `match.route` to the latest route in the `manifest` + - Leverage pre-computed pre-computing flattened/cached route branches during client side route matching + - Performance benchmarks showed roughly a 15-30% improvement in server-side request handling performance + ## v7.14.2 ### Patch Changes diff --git a/packages/react-router/package.json b/packages/react-router/package.json index ed9b2fa660..8be662ef20 100644 --- a/packages/react-router/package.json +++ b/packages/react-router/package.json @@ -1,6 +1,6 @@ { "name": "react-router", - "version": "7.14.2", + "version": "7.15.0", "description": "Declarative routing for React", "keywords": [ "react", From e9d0d4e6110d41b83f4d5e48efc6f2f1fc8dae98 Mon Sep 17 00:00:00 2001 From: Remix Run Bot Date: Tue, 5 May 2026 14:34:56 +0000 Subject: [PATCH 22/23] chore: format --- CHANGELOG.md | 13 ------------- packages/react-router-dev/CHANGELOG.md | 3 --- packages/react-router/CHANGELOG.md | 10 ---------- 3 files changed, 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39610f1442..e1462e3ca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -177,48 +177,37 @@ Date: 2026-05-05 ### Minor Changes - `react-router` - Stabilize `unstable_defaultShouldRevalidate` as `defaultShouldRevalidate` on ``, ``, `useLinkClickHandler`, `useSubmit`, `fetcher.submit`, and `setSearchParams` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `react-router` - Stabilize the instrumentation APIs. `unstable_instrumentations` is now `instrumentations` and `unstable_pattern` is now `pattern` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - The `unstable_ServerInstrumentation`, `unstable_ClientInstrumentation`, `unstable_InstrumentRequestHandlerFunction`, `unstable_InstrumentRouterFunction`, `unstable_InstrumentRouteFunction`, and `unstable_InstrumentationHandlerResult` types have had their `unstable_` prefixes removed - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `react-router` - Stabilize `unstable_mask` as `mask` on ``, `useLinkClickHandler`, and `useNavigate`, and rename the corresponding `Location.unstable_mask` field to `Location.mask` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `react-router` - Stabilize the `unstable_normalizePath` option on `staticHandler.query` and `staticHandler.queryRoute` as `normalizePath` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `react-router` - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `react-router` - Remove `unstable_subResourceIntegrity` from the runtime `FutureConfig` type; the flag is now controlled by the top-level `subResourceIntegrity` option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `react-router` - Stabilize `unstable_url` as `url` on `loader`, `action`, and `middleware` function args ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `react-router` - Stabilize `unstable_useTransitions` as `useTransitions` on ``, ``, ``, ``, ``, ``, ``, and `useLinkClickHandler` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `@react-router/dev` - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `@react-router/dev` - Stabilize `prerender.unstable_concurrency` as `prerender.concurrency` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - `@react-router/dev` - Stabilize `future.unstable_subResourceIntegrity` as a top-level `subResourceIntegrity` config option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly ### Patch Changes @@ -228,7 +217,6 @@ Date: 2026-05-05 - `react-router` - Fix a bug with `unstable_defaultShouldRevalidate={false}` where parent routes that did not export a `shouldRevalidate` function could be incorrectly included in the single fetch call for new child route data ([#15012](https://github.com/remix-run/react-router/pull/15012)) - `react-router` - Improve server-side route matching performance by pre-computing flattened/cached route branches ([#14967](https://github.com/remix-run/react-router/pull/14967)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) - - Performance benchmarks showed roughly a 10-15% improvement in server-side request handling performance - `react-router` - Mark `mask` as an optional field in `Location` for easier mocking in unit tests ([#14999](https://github.com/remix-run/react-router/pull/14999)) @@ -236,7 +224,6 @@ Date: 2026-05-05 - `react-router` - Cache flattened/ranked route branches to optimize server-side route matching ([#14967](https://github.com/remix-run/react-router/pull/14967)) - `react-router` - Improve route matching performance in Framework/Data Mode ([#14971](https://github.com/remix-run/react-router/pull/14971)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) - - Avoiding unnecessary calls to `matchRoutes` in data router scenarios - This includes adding back the optimization that was removed in `7.6.0` ([#13562](https://github.com/remix-run/react-router/pull/13562)) - The issues that prompted the revert have been addressed by using the available router `matches` but always updating `match.route` to the latest route in the `manifest` diff --git a/packages/react-router-dev/CHANGELOG.md b/packages/react-router-dev/CHANGELOG.md index 894a6c44cd..3381692575 100644 --- a/packages/react-router-dev/CHANGELOG.md +++ b/packages/react-router-dev/CHANGELOG.md @@ -5,15 +5,12 @@ ### Minor Changes - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - Stabilize `prerender.unstable_concurrency` as `prerender.concurrency` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - Stabilize `future.unstable_subResourceIntegrity` as a top-level `subResourceIntegrity` config option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly ### Patch Changes diff --git a/packages/react-router/CHANGELOG.md b/packages/react-router/CHANGELOG.md index d0cee515ba..56620bed1c 100644 --- a/packages/react-router/CHANGELOG.md +++ b/packages/react-router/CHANGELOG.md @@ -5,36 +5,28 @@ ### Minor Changes - Stabilize `unstable_defaultShouldRevalidate` as `defaultShouldRevalidate` on ``, ``, `useLinkClickHandler`, `useSubmit`, `fetcher.submit`, and `setSearchParams` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - Stabilize the instrumentation APIs. `unstable_instrumentations` is now `instrumentations` and `unstable_pattern` is now `pattern` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - The `unstable_ServerInstrumentation`, `unstable_ClientInstrumentation`, `unstable_InstrumentRequestHandlerFunction`, `unstable_InstrumentRouterFunction`, `unstable_InstrumentRouteFunction`, and `unstable_InstrumentationHandlerResult` types have had their `unstable_` prefixes removed - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - Stabilize `unstable_mask` as `mask` on ``, `useLinkClickHandler`, and `useNavigate`, and rename the corresponding `Location.unstable_mask` field to `Location.mask` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - Stabilize the `unstable_normalizePath` option on `staticHandler.query` and `staticHandler.queryRoute` as `normalizePath` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - Remove `unstable_subResourceIntegrity` from the runtime `FutureConfig` type; the flag is now controlled by the top-level `subResourceIntegrity` option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - Stabilize `unstable_url` as `url` on `loader`, `action`, and `middleware` function args ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - Stabilize `unstable_useTransitions` as `useTransitions` on ``, ``, ``, ``, ``, ``, ``, and `useLinkClickHandler` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) - - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly ### Patch Changes @@ -44,7 +36,6 @@ - Fix a bug with `unstable_defaultShouldRevalidate={false}` where parent routes that did not export a `shouldRevalidate` function could be incorrectly included in the single fetch call for new child route data ([#15012](https://github.com/remix-run/react-router/pull/15012)) - Improve server-side route matching performance by pre-computing flattened/cached route branches ([#14967](https://github.com/remix-run/react-router/pull/14967)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) - - Performance benchmarks showed roughly a 10-15% improvement in server-side request handling performance - Mark `mask` as an optional field in `Location` for easier mocking in unit tests ([#14999](https://github.com/remix-run/react-router/pull/14999)) @@ -52,7 +43,6 @@ - Cache flattened/ranked route branches to optimize server-side route matching ([#14967](https://github.com/remix-run/react-router/pull/14967)) - Improve route matching performance in Framework/Data Mode ([#14971](https://github.com/remix-run/react-router/pull/14971)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) - - Avoiding unnecessary calls to `matchRoutes` in data router scenarios - This includes adding back the optimization that was removed in `7.6.0` ([#13562](https://github.com/remix-run/react-router/pull/13562)) - The issues that prompted the revert have been addressed by using the available router `matches` but always updating `match.route` to the latest route in the `manifest` From 2ba36dcab76ba973b652f1ad5219816de5e2bc2a Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 5 May 2026 10:46:41 -0400 Subject: [PATCH 23/23] Update release notes --- CHANGELOG.md | 62 +++++++++++++++++++++++++++++----------------------- 1 file changed, 35 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1462e3ca3..0f61b26ae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -174,55 +174,63 @@ We manage release notes in this file instead of the paginated Github Releases Pa Date: 2026-05-05 +### What's Changed + +#### Stabilizations + +We've stabilized a bunch of APIs in this release in preparation for a React Router v8 release hopefully in the next month or two. These flag/prop renames are breaking changes if you've already opted into the unstable APIs so please make sure you make the appropriate changes if so. + +- `future.unstable_passThroughRequests` → `future.v8_passThroughRequests` +- `future.unstable_subResourceIntegrity` → top-level `config.subResourceIntegrity` +- `prerender.unstable_concurrency` → `prerender.concurrency` +- `unstable_url` → `url` (loader, action, middleware, instrumentation args) +- `unstable_instrumentations` → `instrumentations` + - Plus associated types (`ServerInstrumentation`, `ClientInstrumentation`, etc.) +- `unstable_pattern` → `pattern` (loader, action, middleware, instrumentation args) +- `unstable_defaultShouldRevalidate` → `defaultShouldRevalidate` +- `unstable_useTransitions` → `useTransitions` +- `unstable_mask` → `mask` (on ``, `useLinkClickHandler`, `useNavigate`, and `Location`) + +#### Route matching optimizations + +We've added a handful of route matching optimizations in this release for Framework and Data mode. The changes are mostly related to caching the internal flattened/ranked route branches and reducing additional calls to `matchRoutes` along the critical path. This should result in improved performance during both server-side request handling and client-side navigations. + ### Minor Changes -- `react-router` - Stabilize `unstable_defaultShouldRevalidate` as `defaultShouldRevalidate` on ``, ``, `useLinkClickHandler`, `useSubmit`, `fetcher.submit`, and `setSearchParams` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `react-router` - Stabilize `unstable_defaultShouldRevalidate` as `defaultShouldRevalidate` on ``, ``, `useLinkClickHandler`, `useSubmit`, `fetcher.submit`, and `setSearchParams` ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `react-router` - Stabilize the instrumentation APIs. `unstable_instrumentations` is now `instrumentations` and `unstable_pattern` is now `pattern` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `react-router` - Stabilize the instrumentation APIs ([14999](https://github.com/remix-run/react-router/pull/14999)) + - `unstable_instrumentations` is now `instrumentations` + - `unstable_pattern` is now `pattern` - The `unstable_ServerInstrumentation`, `unstable_ClientInstrumentation`, `unstable_InstrumentRequestHandlerFunction`, `unstable_InstrumentRouterFunction`, `unstable_InstrumentRouteFunction`, and `unstable_InstrumentationHandlerResult` types have had their `unstable_` prefixes removed - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `react-router` - Stabilize `unstable_mask` as `mask` on ``, `useLinkClickHandler`, and `useNavigate`, and rename the corresponding `Location.unstable_mask` field to `Location.mask` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `react-router` - Stabilize `unstable_mask` as `mask` on ``, `useLinkClickHandler`, and `useNavigate`, and rename the corresponding `Location.unstable_mask` field to `Location.mask` ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `react-router` - Stabilize the `unstable_normalizePath` option on `staticHandler.query` and `staticHandler.queryRoute` as `normalizePath` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `react-router` - Stabilize the `unstable_normalizePath` option on `staticHandler.query` and `staticHandler.queryRoute` as `normalizePath` ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `react-router` - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `react-router` - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `react-router` - Remove `unstable_subResourceIntegrity` from the runtime `FutureConfig` type; the flag is now controlled by the top-level `subResourceIntegrity` option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `react-router` - Remove `unstable_subResourceIntegrity` from the runtime `FutureConfig` type; the flag is now controlled by the top-level `subResourceIntegrity` option in `react-router.config.ts` ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `react-router` - Stabilize `unstable_url` as `url` on `loader`, `action`, and `middleware` function args ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `react-router` - Stabilize `unstable_url` as `url` on `loader`, `action`, and `middleware` function args ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `react-router` - Stabilize `unstable_useTransitions` as `useTransitions` on ``, ``, ``, ``, ``, ``, ``, and `useLinkClickHandler` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `react-router` - Stabilize `unstable_useTransitions` as `useTransitions` on ``, ``, ``, ``, ``, ``, ``, and `useLinkClickHandler` ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `@react-router/dev` - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `@react-router/dev` - Stabilize `future.unstable_passThroughRequests` as `future.v8_passThroughRequests` ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `@react-router/dev` - Stabilize `prerender.unstable_concurrency` as `prerender.concurrency` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `@react-router/dev` - Stabilize `prerender.unstable_concurrency` as `prerender.concurrency` ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly - -- `@react-router/dev` - Stabilize `future.unstable_subResourceIntegrity` as a top-level `subResourceIntegrity` config option in `react-router.config.ts` ([a993f09](https://github.com/remix-run/react-router/commit/a993f09)) +- `@react-router/dev` - Stabilize `future.unstable_subResourceIntegrity` as a top-level `subResourceIntegrity` config option in `react-router.config.ts` ([14999](https://github.com/remix-run/react-router/pull/14999)) - ⚠️ This is a breaking change if you have already opted into the unstable version - you will need to update your code accordingly ### Patch Changes - `react-router` - Add `nonce` to `` `` elements (if provided) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) - - `react-router` - Fix a bug with `unstable_defaultShouldRevalidate={false}` where parent routes that did not export a `shouldRevalidate` function could be incorrectly included in the single fetch call for new child route data ([#15012](https://github.com/remix-run/react-router/pull/15012)) - +- `react-router` - Mark `mask` as an optional field in `Location` for easier mocking in unit tests ([#14999](https://github.com/remix-run/react-router/pull/14999)) - `react-router` - Improve server-side route matching performance by pre-computing flattened/cached route branches ([#14967](https://github.com/remix-run/react-router/pull/14967)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) - Performance benchmarks showed roughly a 10-15% improvement in server-side request handling performance - -- `react-router` - Mark `mask` as an optional field in `Location` for easier mocking in unit tests ([#14999](https://github.com/remix-run/react-router/pull/14999)) - - `react-router` - Cache flattened/ranked route branches to optimize server-side route matching ([#14967](https://github.com/remix-run/react-router/pull/14967)) - - `react-router` - Improve route matching performance in Framework/Data Mode ([#14971](https://github.com/remix-run/react-router/pull/14971)) ([af5d49b](https://github.com/remix-run/react-router/commit/af5d49b)) - Avoiding unnecessary calls to `matchRoutes` in data router scenarios - This includes adding back the optimization that was removed in `7.6.0` ([#13562](https://github.com/remix-run/react-router/pull/13562))