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"; 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.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/.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/__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)); } 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, }, );