diff --git a/docs/api/rsc/RSCHydratedRouter.md b/docs/api/rsc/RSCHydratedRouter.md index 1c4f15239c..059a1d8696 100644 --- a/docs/api/rsc/RSCHydratedRouter.md +++ b/docs/api/rsc/RSCHydratedRouter.md @@ -62,7 +62,6 @@ function RSCHydratedRouter({ createFromReadableStream, fetch: fetchImplementation = fetch, payload, - routeDiscovery = "eager", getContext, }: RSCHydratedRouterProps) ``` @@ -90,8 +89,3 @@ navigation or fetcher call. The decoded [`unstable_RSCPayload`](https://api.reactrouter.com/v7/types/react-router.unstable_RSCPayload.html) to hydrate. -### routeDiscovery - -`"eager"` or `"lazy"` - Determines if links are eagerly discovered, or -delayed until clicked. - diff --git a/docs/api/rsc/matchRSCServerRequest.md b/docs/api/rsc/matchRSCServerRequest.md index cd78bba518..7493ad46ba 100644 --- a/docs/api/rsc/matchRSCServerRequest.md +++ b/docs/api/rsc/matchRSCServerRequest.md @@ -75,6 +75,7 @@ async function matchRSCServerRequest({ basename, decodeReply, requestContext, + routeDiscovery, loadServerAction, decodeAction, decodeFormState, @@ -94,6 +95,7 @@ async function matchRSCServerRequest({ onError?: (error: unknown) => void; request: Request; routes: RSCRouteConfigEntry[]; + routeDiscovery?: RouteDiscovery; generateResponse: ( match: RSCMatch, { @@ -158,6 +160,10 @@ The [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) to mat An instance of [`RouterContextProvider`](../utils/RouterContextProvider) that should be created per request, to be passed to [`action`](../../start/data/route-object#action)s, [`loader`](../../start/data/route-object#loader)s and [middleware](../../how-to/middleware). +### opts.routeDiscovery + +The route discovery configuration, used to determine how the router should discover new routes during navigations. + ### opts.routes Your [route definitions](https://api.reactrouter.com/v7/types/react-router.unstable_RSCRouteConfigEntry.html). diff --git a/docs/api/utils/redirect.md b/docs/api/utils/redirect.md index adea0cb254..8174996790 100644 --- a/docs/api/utils/redirect.md +++ b/docs/api/utils/redirect.md @@ -26,6 +26,9 @@ A redirect [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Respons Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302). +This utility accepts absolute URLs and can navigate to external domains, so +the application should validate any user-supplied inputs to redirects. + ```tsx import { redirect } from "react-router"; diff --git a/docs/api/utils/redirectDocument.md b/docs/api/utils/redirectDocument.md index b985d5d228..7bf06bb861 100644 --- a/docs/api/utils/redirectDocument.md +++ b/docs/api/utils/redirectDocument.md @@ -27,6 +27,9 @@ that will force a document reload to the new location. Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302). +This utility accepts absolute URLs and can navigate to external domains, so +the application should validate any user-supplied inputs to redirects. + ```tsx filename=routes/logout.tsx import { redirectDocument } from "react-router"; diff --git a/docs/how-to/react-server-components.md b/docs/how-to/react-server-components.md index 0841899632..1773361613 100644 --- a/docs/how-to/react-server-components.md +++ b/docs/how-to/react-server-components.md @@ -77,7 +77,7 @@ export default defineConfig({ ### Build Output -The RSC Framework Mode server build file (`build/server/index.js`) now exports a `default` request handler function (`(request: Request) => Promise`) for document/data requests. +The RSC Framework Mode server build file (`build/server/index.js`) exports a `default` request handler function (`(request: Request) => Promise`) for document/data requests. If needed, you can convert this into a [standard Node.js request listener][node-request-listener] for use with Node's built-in `http.createServer` function (or anything that supports it, e.g. [Express][express]) by using the `createRequestListener` function from [@remix-run/node-fetch-server][node-fetch-server]. @@ -104,7 +104,7 @@ app.listen(3000); ### React Elements From Loaders/Actions -In RSC Framework Mode, loaders and actions can now return React elements along with other data. These elements will only ever be rendered on the server. +In RSC Framework Mode, loaders and actions can return React elements along with other data. These elements will only ever be rendered on the server. ```tsx import type { Route } from "./+types/route"; @@ -133,6 +133,8 @@ If you need to use client-only features (e.g. [Hooks][hooks], event handlers) wi ```tsx filename=src/routes/counter/counter.tsx "use client"; +import { useState } from "react"; + export function Counter() { const [count, setCount] = useState(0); return ( @@ -173,7 +175,9 @@ export default function Route({ ### Route Server Components -If a route exports a `ServerComponent` instead of the typical `default` component export, this will be a server component rather than the usual client component. A default export and `ServerComponent` can not both be exported from the same route module, but you can still export client-only annotations like `clientLoader` and `clientAction` alongside a `ServerComponent`, along with any other component exports such as `ErrorBoundary` or `Layout`. +If a route exports a `ServerComponent` instead of the typical `default` component export, the route renders on the server instead of the client. A route module cannot export both `default` and `ServerComponent`. + +You can still export client-only annotations like `clientLoader` and `clientAction` alongside a `ServerComponent`. The other route module component exports follow the same client/server split: `ErrorBoundary`, `Layout`, and `HydrateFallback` are client components, while `ServerErrorBoundary`, `ServerLayout`, and `ServerHydrateFallback` render on the server. The following route module components have their own mutually exclusive server component counterparts: @@ -213,6 +217,8 @@ If you need to use client-only features (e.g. [Hooks][hooks], event handlers) wi ```tsx filename=src/routes/counter/counter.tsx "use client"; +import { useState } from "react"; + export function Counter() { const [count, setCount] = useState(0); return ( @@ -287,6 +293,8 @@ The plugin will automatically detect custom entry files in your `app` directory: If these files are not found, React Router will use the default entries provided by the framework. +If you want to inspect the generated defaults before overriding them, you can also use `react-router reveal entry.client`, `react-router reveal entry.rsc`, and `react-router reveal entry.ssr`. + #### Basic Override Pattern You can create a custom entry file that wraps or extends the default behavior. For example, to add custom logging to the RSC entry: @@ -371,14 +379,11 @@ When copying default entries, make sure to maintain the required exports: ### Unsupported Config Options -For the initial unstable release, the following options from `react-router.config.ts` are not yet supported in RSC Framework Mode: +The following options from `react-router.config.ts` are not currently supported in RSC Framework Mode: - `buildEnd` -- `prerender` - `presets` -- `routeDiscovery` - `serverBundles` -- `ssr: false` (SPA Mode) - `future.v8_splitRouteModules` - `future.unstable_subResourceIntegrity` @@ -403,11 +408,13 @@ matchRSCServerRequest({ }); ``` -While you can define components inline, we recommend using the `lazy()` option and defining [Route Modules][route-module] for both startup performance and code organization +While you can define components inline, we recommend using the `lazy()` option and defining [Route Modules][route-module] for both startup performance and code organization. -The [Route Module API][route-module] up until now has been a [Framework Mode][framework-mode] only feature. However, the `lazy` field of the RSC route config expects the same exports as the Route Module exports, unifying the APIs even further. +The `lazy` field of the RSC route config expects the same exports as the [Route Module API][route-module], which keeps the route-module shape consistent across [Framework Mode][framework-mode] and RSC Data Mode. + +That includes exports like `loader`, `action`, `meta`, `links`, `headers`, `ErrorBoundary`, `HydrateFallback`, and the client annotations. @@ -542,6 +549,10 @@ export function clientAction() {} export function clientLoader() {} export function shouldRevalidate() {} + +export default function ClientRoot() { + return

Client route

; +} ``` We can then re-export these from our lazy loaded route module: @@ -551,7 +562,7 @@ export { clientAction, clientLoader, shouldRevalidate, -} from "./route.client"; +} from "./client"; export default function Root() { // ... @@ -566,7 +577,7 @@ export { clientAction, clientLoader, shouldRevalidate, -} from "./route.client"; +} from "./client"; export default function Root() { // Adding a Server Component at the root is required by bundlers @@ -721,8 +732,12 @@ export async function generateHTML( // Provide the React Server touchpoints. createFromReadableStream, // Render the router to HTML. - async renderHTML(getPayload) { - const payload = getPayload(); + async renderHTML(getPayload, options) { + const payload = await getPayload(); + const formState = + payload.type === "render" + ? await payload.formState + : undefined; const bootstrapScriptContent = await import.meta.viteRsc.loadBootstrapScriptContent( @@ -732,8 +747,10 @@ export async function generateHTML( return await renderHTMLToReadableStream( , { + ...options, bootstrapScriptContent, - formState: payload.formState, + formState, + signal: request.signal, }, ); }, @@ -771,9 +788,9 @@ function fetchServer(request: Request) { // The app routes. routes: routes(), // Encode the match with the React Server implementation. - generateResponse(match) { + generateResponse(match, options) { return new Response( - renderToReadableStream(match.payload), + renderToReadableStream(match.payload, options), { status: match.statusCode, headers: match.headers, @@ -811,8 +828,8 @@ import { unstable_createCallServer as createCallServer, unstable_getRSCStream as getRSCStream, unstable_RSCHydratedRouter as RSCHydratedRouter, - type unstable_RSCPayload as RSCServerPayload, -} from "react-router"; + type unstable_RSCPayload as RSCPayload, +} from "react-router/dom"; // Create and set the callServer function to support post-hydration server actions. setServerCallback( @@ -824,31 +841,31 @@ setServerCallback( ); // Get and decode the initial server payload. -createFromReadableStream( - getRSCStream(), -).then((payload) => { - startTransition(async () => { - const formState = - payload.type === "render" - ? await payload.formState - : undefined; - - hydrateRoot( - document, - - - , - { - formState, - }, - ); - }); -}); +createFromReadableStream(getRSCStream()).then( + (payload) => { + startTransition(async () => { + const formState = + payload.type === "render" + ? await payload.formState + : undefined; + + hydrateRoot( + document, + + + , + { + formState, + }, + ); + }); + }, +); ``` [picking-a-mode]: ../start/modes diff --git a/packages/react-router/lib/router/utils.ts b/packages/react-router/lib/router/utils.ts index 09345aaabe..39b0d8b926 100644 --- a/packages/react-router/lib/router/utils.ts +++ b/packages/react-router/lib/router/utils.ts @@ -1937,6 +1937,9 @@ export type RedirectFunction = ( * Sets the status code and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) * header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302). * + * This utility accepts absolute URLs and can navigate to external domains, so + * the application should validate any user-supplied inputs to redirects. + * * @example * import { redirect } from "react-router"; * @@ -1979,6 +1982,9 @@ export const redirect: RedirectFunction = (url, init = 302) => { * and the [`Location`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location) * header. Defaults to [`302 Found`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302). * + * This utility accepts absolute URLs and can navigate to external domains, so + * the application should validate any user-supplied inputs to redirects. + * * ```tsx filename=routes/logout.tsx * import { redirectDocument } from "react-router"; *