From 5fe18b0cbe6dd262b96f64d74cc09e8d40b7dbd0 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Tue, 30 Jun 2026 16:28:53 -0400 Subject: [PATCH 1/3] Update docs diff code fences (#15261) Assisted-By: devx/a8fb5f6d-882e-4977-a6ac-8b182b117c08 --- docs/explanation/hot-module-replacement.md | 2 +- docs/how-to/data-strategy.md | 2 +- docs/upgrading/component-routes.md | 2 +- docs/upgrading/router-provider.md | 10 +++++----- docs/upgrading/v7.md | 14 +++++++------- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/explanation/hot-module-replacement.md b/docs/explanation/hot-module-replacement.md index 0467cb7719..800206129c 100644 --- a/docs/explanation/hot-module-replacement.md +++ b/docs/explanation/hot-module-replacement.md @@ -111,7 +111,7 @@ export default function Component({ loaderData }) { If you change the key `pet` to `dog`: -```diff +```tsx diff export default function Component() { - const { pet } = useMyCustomHook(); + const { dog } = useMyCustomHook(); diff --git a/docs/how-to/data-strategy.md b/docs/how-to/data-strategy.md index 288db27906..bdc3b57014 100644 --- a/docs/how-to/data-strategy.md +++ b/docs/how-to/data-strategy.md @@ -166,7 +166,7 @@ With `shouldCallHandler`, you are in charge of which handlers should be called s Here's an example change from the prior API to the new API. Note that we pre-filter the `matchesToLoad` before calling `resolve()`: -```diff +```tsx diff let results = {}; +let matchesToLoad = matches.filter(m => m.shouldCallHandler()); await Promise.all(() => diff --git a/docs/upgrading/component-routes.md b/docs/upgrading/component-routes.md index c23a002591..b03eabe181 100644 --- a/docs/upgrading/component-routes.md +++ b/docs/upgrading/component-routes.md @@ -49,7 +49,7 @@ npm install @react-router/node **👉 Swap out the React plugin for React Router.** -```diff filename=vite.config.ts +```tsx diff filename=vite.config.ts -import react from '@vitejs/plugin-react' +import { reactRouter } from "@react-router/dev/vite"; import { defineConfig } from "vite"; diff --git a/docs/upgrading/router-provider.md b/docs/upgrading/router-provider.md index 4df380db98..89c1379081 100644 --- a/docs/upgrading/router-provider.md +++ b/docs/upgrading/router-provider.md @@ -85,7 +85,7 @@ Instead of importing your route modules directly, lazy load and convert them to Not only does your route definition now conform to the Route Module API, but you also get the benefits of code-splitting your routes. -```diff filename=src/main.tsx +```tsx diff filename=src/main.tsx let router = createBrowserRouter([ // ... other routes { @@ -120,7 +120,7 @@ npm install @react-router/node **👉 Swap out the React plugin for React Router** -```diff filename=vite.config.ts +```tsx diff filename=vite.config.ts -import react from '@vitejs/plugin-react' +import { reactRouter } from "@react-router/dev/vite"; import { defineConfig } from "vite"; @@ -247,7 +247,7 @@ export default function App() { You would move everything above the `RouterProvider` into `root.tsx`. -```diff filename=src/root.tsx +```tsx diff filename=src/root.tsx +import "./index.css"; // ... other imports and Layout @@ -326,7 +326,7 @@ touch src/routes.ts src/catchall.tsx Move your route definitions to `routes.ts`. Note that the schemas don't match exactly, so you will get type errors; we'll fix this next. -```diff filename=src/routes.ts +```tsx diff filename=src/routes.ts +import type { RouteConfig } from "@react-router/dev/routes"; -const router = createBrowserRouter([ @@ -362,7 +362,7 @@ Move your route definitions to `routes.ts`. Note that the schemas don't match ex **👉 Replace the `lazy` loader with a `file` loader** -```diff filename=src/routes.ts +```tsx diff filename=src/routes.ts export default [ { path: "/", diff --git a/docs/upgrading/v7.md b/docs/upgrading/v7.md index e6e8cec736..4a19366016 100644 --- a/docs/upgrading/v7.md +++ b/docs/upgrading/v7.md @@ -140,7 +140,7 @@ Most users won't need to make any changes. However, if you have custom Vite conf For example, a custom server build should move its SSR `rollupOptions` from the top-level `build` config into `environments.ssr.build`: -```diff filename=vite.config.ts +```tsx diff filename=vite.config.ts import { reactRouter } from "@react-router/dev/vite"; import { defineConfig } from "vite"; @@ -310,7 +310,7 @@ Use `loaderData` instead on `MetaArgs`, each item in `MetaArgs.matches`, and eac Replace `data` with `loaderData` in your `meta` functions: -```diff +```tsx diff export function meta({ - data, + loaderData, @@ -327,7 +327,7 @@ export function meta({ If you read data from parent matches, update those references too: -```diff +```tsx diff export function meta({ matches }: Route.MetaArgs) { let rootMatch = matches.find((match) => match.id === "root"); - let rootData = rootMatch?.data; @@ -339,7 +339,7 @@ export function meta({ matches }: Route.MetaArgs) { Replace `data` with `loaderData` on `useMatches()` calls: -```diff +```tsx diff export default function Component({ matches, loaderData }: ComponentProps) { let matches = useMatches(); - const rootLoaderData = matches[0].data; @@ -369,14 +369,14 @@ npm uninstall react-router-dom Replace `react-router-dom` imports with `react-router` imports: -```diff +```tsx diff -import { Link, useLocation } from "react-router-dom"; +import { Link, useLocation } from "react-router"; ``` For DOM-specific APIs, import from `react-router/dom`: -```diff +```tsx diff -import { RouterProvider } from "react-router-dom"; +import { RouterProvider } from "react-router/dom"; ``` @@ -396,7 +396,7 @@ React Router v8 removes the React Router Cloudflare dev proxy. Cloudflare projec Replace `cloudflareDevProxy` with `cloudflare`: -```diff filename=vite.config.ts +```tsx diff filename=vite.config.ts import { reactRouter } from "@react-router/dev/vite"; -import { cloudflareDevProxy } from "@react-router/dev/vite/cloudflare"; +import { cloudflare } from "@cloudflare/vite-plugin"; From ec6d3a290c7cc67675c9718596f1ced0607759bf Mon Sep 17 00:00:00 2001 From: Jacob Ebey Date: Tue, 30 Jun 2026 13:51:28 -0700 Subject: [PATCH 2/3] fix: update to use ReactFormState types instead of unknown. (#15263) --- integration/helpers/rsc-vite/src/entry.browser.tsx | 4 +--- .../react-router/.changes/patch.rsc-form-state.md | 1 + packages/react-router/lib/rsc/server.rsc.ts | 13 +++++++------ playground/rsc-vite-framework/app/entry.client.tsx | 1 - playground/rsc-vite-framework/app/entry.ssr.tsx | 2 -- playground/rsc-vite/src/entry.browser.tsx | 1 - 6 files changed, 9 insertions(+), 13 deletions(-) create mode 100644 packages/react-router/.changes/patch.rsc-form-state.md diff --git a/integration/helpers/rsc-vite/src/entry.browser.tsx b/integration/helpers/rsc-vite/src/entry.browser.tsx index 23949596e4..647d79eacd 100644 --- a/integration/helpers/rsc-vite/src/entry.browser.tsx +++ b/integration/helpers/rsc-vite/src/entry.browser.tsx @@ -24,8 +24,7 @@ setServerCallback( createFromReadableStream(getRSCStream()).then((payload) => { startTransition(async () => { - const formState = - payload.type === "render" ? await payload.formState : undefined; + const formState = payload.type === "render" ? payload.formState : undefined; hydrateRoot( document, @@ -37,7 +36,6 @@ createFromReadableStream(getRSCStream()).then((payload) => { /> , { - // @ts-expect-error - no types for this yet formState, }, ); diff --git a/packages/react-router/.changes/patch.rsc-form-state.md b/packages/react-router/.changes/patch.rsc-form-state.md new file mode 100644 index 0000000000..1bf83a8517 --- /dev/null +++ b/packages/react-router/.changes/patch.rsc-form-state.md @@ -0,0 +1 @@ +Update to use ReactFormState types instead of unknown. diff --git a/packages/react-router/lib/rsc/server.rsc.ts b/packages/react-router/lib/rsc/server.rsc.ts index 55fe9240aa..80cc86fa04 100644 --- a/packages/react-router/lib/rsc/server.rsc.ts +++ b/packages/react-router/lib/rsc/server.rsc.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line import/no-nodejs-modules import { AsyncLocalStorage } from "node:async_hooks"; import * as React from "react"; +import type { ReactFormState } from "react-dom/client"; import type { ClientActionFunction, @@ -253,7 +254,7 @@ export type RSCRenderPayload = { // for SPA navigations the manifest call will handle these patches. patches?: Promise; nonce?: string; - formState?: unknown; + formState?: ReactFormState; }; export type RSCManifestPayload = { @@ -296,7 +297,7 @@ export type DecodeActionFunction = ( export type DecodeFormStateFunction = ( result: unknown, formData: FormData, -) => unknown; +) => Promise; export type DecodeReplyFunction = ( reply: FormData | string, @@ -638,7 +639,7 @@ async function processServerAction( skipRevalidation: boolean; revalidationRequest: Request; actionResult?: Promise; - formState?: unknown; + formState?: ReactFormState; } | Response | undefined @@ -711,7 +712,7 @@ async function processServerAction( if (isRedirectResponse(result)) { result = prependBasenameToRedirectResponse(result, basename); } - formState = decodeFormState?.(result, formData); + formState = await decodeFormState?.(result, formData); } catch (error) { if (isRedirectResponse(error)) { return prependBasenameToRedirectResponse(error, basename); @@ -850,7 +851,7 @@ async function generateRenderResponse( // revalidation. If this is a RR Form/Fetcher submission, // `processServerAction` will fall through as a no-op and we'll pass the // POST `request` to `query` and process our action there. - let formState: unknown; + let formState: ReactFormState | undefined; let skipRevalidation = false; let potentialCSRFAttackError: unknown | undefined; if (isMutationMethod(request.method)) { @@ -1039,7 +1040,7 @@ async function generateStaticContextResponse( isDataRequest: boolean, isSubmission: boolean, actionResult: Promise | undefined, - formState: unknown | undefined, + formState: ReactFormState | undefined, staticContext: StaticHandlerContext, temporaryReferences: unknown, skipRevalidation: boolean, diff --git a/playground/rsc-vite-framework/app/entry.client.tsx b/playground/rsc-vite-framework/app/entry.client.tsx index cfad7099d2..ed22b48d77 100644 --- a/playground/rsc-vite-framework/app/entry.client.tsx +++ b/playground/rsc-vite-framework/app/entry.client.tsx @@ -39,7 +39,6 @@ createFromReadableStream(getRSCStream()).then((payload) => { /> , { - // @ts-expect-error - on 18 types, requires 19. --- IGNORE --- formState, }, ); diff --git a/playground/rsc-vite-framework/app/entry.ssr.tsx b/playground/rsc-vite-framework/app/entry.ssr.tsx index e03b086b78..b56600eefa 100644 --- a/playground/rsc-vite-framework/app/entry.ssr.tsx +++ b/playground/rsc-vite-framework/app/entry.ssr.tsx @@ -1,5 +1,4 @@ import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr"; -// @ts-ignore - on 18 types, requires 19 import { renderToReadableStream } from "react-dom/server.edge"; import { unstable_routeRSCServerRequest as routeRSCServerRequest, @@ -32,7 +31,6 @@ export async function generateHTML( { ...options, bootstrapScriptContent, - // @ts-expect-error - no types for this yet formState, signal: request.signal, }, diff --git a/playground/rsc-vite/src/entry.browser.tsx b/playground/rsc-vite/src/entry.browser.tsx index fe0ad7a3f8..3c9c8e8284 100644 --- a/playground/rsc-vite/src/entry.browser.tsx +++ b/playground/rsc-vite/src/entry.browser.tsx @@ -35,7 +35,6 @@ createFromReadableStream(getRSCStream()).then((payload) => { /> , { - // @ts-expect-error - no types for this yet formState, }, ); From 43e8079f32095026b302caf4dd150cf21872262e Mon Sep 17 00:00:00 2001 From: dfedoryshchev <64079946+dfedoryshchev@users.noreply.github.com> Date: Tue, 30 Jun 2026 22:10:28 +0100 Subject: [PATCH 3/3] fix(router): preserve navigation blocker state through a revalidation (#15246) --- ...navigation-blocker-state-through-revalidation.md | 1 + .../__tests__/router/navigation-blocking-test.ts | 13 +++++++++++++ packages/react-router/lib/router/router.ts | 5 +++-- 3 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 packages/react-router/.changes/patch.preserve-navigation-blocker-state-through-revalidation.md diff --git a/packages/react-router/.changes/patch.preserve-navigation-blocker-state-through-revalidation.md b/packages/react-router/.changes/patch.preserve-navigation-blocker-state-through-revalidation.md new file mode 100644 index 0000000000..634adafd2d --- /dev/null +++ b/packages/react-router/.changes/patch.preserve-navigation-blocker-state-through-revalidation.md @@ -0,0 +1 @@ +Preserve navigation blocker state through a revalidation diff --git a/packages/react-router/__tests__/router/navigation-blocking-test.ts b/packages/react-router/__tests__/router/navigation-blocking-test.ts index 4406434bd4..7a17bc45b6 100644 --- a/packages/react-router/__tests__/router/navigation-blocking-test.ts +++ b/packages/react-router/__tests__/router/navigation-blocking-test.ts @@ -175,6 +175,19 @@ describe("navigation blocking", () => { expect(router.state.location.pathname).toBe(pathnameBeforeNavigation); }); }); + + describe("revalidation while blocked", () => { + let fn = () => true; + it("preserves a 'blocked' blocker through a revalidation", async () => { + router.getBlocker("KEY", fn); + await router.navigate("/about"); + expect(router.getBlocker("KEY", fn).state).toBe("blocked"); + + // a revalidation is not a navigation, so it must not clear the blocker + await router.revalidate(); + expect(router.getBlocker("KEY", fn).state).toBe("blocked"); + }); + }); }); describe("on history replace", () => { diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index 7e530e4c3e..1a56d58f1b 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -1544,9 +1544,10 @@ export function createRouter(init: RouterInit): Router { : state.loaderData; // On a successful navigation we can assume we got through all blockers - // so we can start fresh + // so we can start fresh. A revalidation is not a navigation, so it must + // leave any active blocker untouched. let blockers = state.blockers; - if (blockers.size > 0) { + if (blockers.size > 0 && !isUninterruptedRevalidation) { blockers = new Map(blockers); blockers.forEach((_, k) => blockers.set(k, IDLE_BLOCKER)); }