Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/dev' into mwilcox/skip-importe…
Browse files Browse the repository at this point in the history
…d-route-typegen

* upstream/dev:
  bump patch to minor for new API: `href`
  Type-safe href (#12994)
  docs: prerender/ssr:false (#13005)
  Skip action-only resource routes with prerender:true (#13004)
  Update docs for spa/prerendering
  Improvements to ssr:false + prerender scenarios (#12948)
  • Loading branch information
wilcoxmd committed Feb 12, 2025
2 parents 9d0dc64 + cd39c4f commit 7f1f4b9
Show file tree
Hide file tree
Showing 47 changed files with 2,486 additions and 956 deletions.
16 changes: 16 additions & 0 deletions .changeset/dull-balloons-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@react-router/dev": patch
"react-router": patch
---

Fix typegen for repeated params

In React Router, path parameters are keyed by their name.
So for a path pattern like `/a/:id/b/:id?/c/:id`, the last `:id` will set the value for `id` in `useParams` and the `params` prop.
For example, `/a/1/b/2/c/3` will result in the value `{ id: 3 }` at runtime.

Previously, generated types for params incorrectly modeled repeated params with an array.
So `/a/1/b/2/c/3` generated a type like `{ id: [1,2,3] }`.

To be consistent with runtime behavior, the generated types now correctly model the "last one wins" semantics of path parameters.
So `/a/1/b/2/c/3` now generates a type like `{ id: 3 }`.
5 changes: 5 additions & 0 deletions .changeset/fresh-buttons-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

Don't apply Single Fetch revalidation de-optimization when in SPA mode since there is no server HTTP request
5 changes: 5 additions & 0 deletions .changeset/khaki-rocks-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Fix `ArgError: unknown or unexpected option: --version` when running `react-router --version`
5 changes: 5 additions & 0 deletions .changeset/late-nails-hear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Skip action-only resource routes when using `prerender:true`
12 changes: 12 additions & 0 deletions .changeset/prerender-invalid-exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@react-router/dev": patch
---

Enhance invalid export detection when using `ssr:false`

- `headers`/`action` are prohibited in all routes with `ssr:false` because there will be no runtime server on which to run them
- `loader` functions are more nuanced and depend on whether a given route is prerendered
- When using `ssr:false` without a `prerender` config, only the `root` route can have a `loader`
- This is "SPA mode" which generates a single `index.html` file with the root route `HydrateFallback` so it is capable of hydrating for any path in your application - therefore we can only call a root route `loader` at build time
- When using `ssr:false` with a `prerender` config, you can export a `loader` from routes matched by one of the `prerender` paths because those routes will be server rendered at build time
- Exporting a `loader` from a route that is never matched by a `prerender` path will throw a build time error because there will be no runtime server to ever run the loader
13 changes: 13 additions & 0 deletions .changeset/prerender-spa-fallback.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
"@react-router/dev": minor
---

Generate a "SPA fallback" HTML file for scenarios where applications are prerendering the `/` route with `ssr:false`

- If you specify `ssr:false` without a `prerender` config, this is considered "SPA Mode" and the generated `index.html` file will only render down to the root route and will be able to hydrate for any valid application path
- If you specify `ssr:false` with a `prerender` config but _do not_ include the `/` path (i.e., `prerender: ['/blog/post']`), then we still generate a "SPA Mode" `index.html` file that can hydrate for any path in the application
- However, previously if you specified `ssr:false` and included the `/` path in your `prerender` config, we would prerender the `/` route into `index.html` as a non-SPA page
- The generated HTML would include the root index route which prevented hydration for any other paths
- With this change, we now generate a "SPA Mode" file in `__spa-fallback.html` that will allow you to hydrate for any non-prerendered paths
- You can serve this file from your static file server for any paths that would otherwise 404 if you only want to pre-render _some_ routes in your `ssr:false` app and serve the others as a SPA
- `npx sirv-cli build/client --single __spa-fallback.html`
5 changes: 5 additions & 0 deletions .changeset/rotten-numbers-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@react-router/dev": patch
---

Limit prerendered resource route `.data` files to only the target route
9 changes: 9 additions & 0 deletions .changeset/spa-mode-root-loader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@react-router/dev": minor
---

- Allow a `loader` in the root route in SPA mode because it can be called/server-rendered at build time
- `Route.HydrateFallbackProps` now also receives `loaderData`
- This will be defined so long as the `HydrateFallback` is rendering while _children_ routes are loading
- This will be `undefined` if the `HydrateFallback` is rendering because the route has it's own hydrating `clientLoader`
- In SPA mode, this will allow you to render loader root data into the SPA `index.html`
9 changes: 9 additions & 0 deletions .changeset/stale-ways-ring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"react-router": patch
---

Align dev server behavior with static file server behavior when `ssr:false` is set

- When no `prerender` config exists, only SSR down to the root `HydrateFallback` (SPA Mode)
- When a `prerender` config exists but the current path is not prerendered, only SSR down to the root `HydrateFallback` (SPA Fallback)
- Return a 404 on `.data` requests to non-pre-rendered paths
20 changes: 20 additions & 0 deletions .changeset/three-eyes-flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@react-router/dev": minor
"react-router": minor
---

New type-safe `href` utility that guarantees links point to actual paths in your app

```tsx
import { href } from "react-router";

export default function Component() {
const link = href("/blog/:slug", { slug: "my-first-post" });
return (
<main>
<Link to={href("/products/:id", { id: "asdf" })} />
<NavLink to={href("/:lang?/about", { lang: "en" })} />
</main>
);
}
```
99 changes: 87 additions & 12 deletions docs/how-to/pre-rendering.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,45 @@ title: Pre-Rendering

# Pre-Rendering

Pre-rendering allows you to render pages at build time instead of on a server to speed up pages loads for static content.
Pre-Rendering allows you to speed up page loads for static content by rendering pages at build time instead of at runtime. Pre-rendering is enabled via the `prerender` config in `react-router.config.ts` and can be used in two ways based on the `ssr` config value:

## Configuration
- Alongside a runtime SSR server ith `ssr:true` (the default value)
- Deployed to a static file server with `ssr:false`

## Pre-rendering with `ssr:true`

### Configuration

Add the `prerender` option to your config, there are three signatures:

```ts filename=react-router.config.ts
```ts filename=react-router.config.ts lines=[7-8,10-11,13-21]
import type { Config } from "@react-router/dev/config";

export default {
// all static route paths
// (no dynamic segments like "/post/:slug")
// Can be omitted - defaults to true
ssr: true,

// all static paths (no dynamic segments like "/post/:slug")
prerender: true,

// any url
// specific paths
prerender: ["/", "/blog", "/blog/popular-post"],

// async function for dependencies like a CMS
async prerender({ getStaticPaths }) {
let posts = await fakeGetPostsFromCMS();
return ["/", "/blog"].concat(
posts.map((post) => post.href)
);
return [
"/",
"/blog",
...posts.map((post) => post.href),
];
},
} satisfies Config;
```

## Data Loading and Pre-rendering
### Data Loading and Pre-rendering

There is no extra application API for pre-rendering. Pre-rendering uses the same route loaders as server rendering:
There is no extra application API for pre-rendering. Routes being pre-rendered use the same route `loader` functions as server rendering:

```tsx
export async function loader({ request, params }) {
Expand All @@ -50,7 +59,7 @@ Instead of a request coming to your route on a deployed server, the build create

When server rendering, requests to paths that have not been pre-rendered will be server rendered as usual.

## Static File Output
### Static File Output

The rendered result will be written out to your `build/client` directory. You'll notice two files for each path:

Expand All @@ -74,3 +83,69 @@ Prerender: Generated build/client/blog/my-first-post/index.html
```

During development, pre-rendering doesn't save the rendered results to the public directory, this only happens for `react-router build`.

## Pre-rendering with `ssr:false`

The above examples assume you are deploying a runtime server, but are pre-rendering some static pages in order to serve them faster and avoid hitting the server.

To disable runtime SSR and configure pre-rendering to be served from a static file server, you can set the `ssr:false` config flag:

```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
ssr: false, // disable runtime server rendering
prerender: true, // pre-render all static routes
} satisfies Config;
```

If you specify `ssr:false` without a `prerender` config, React Router refers to that as [SPA Mode](./spa). In SPA Mode, we render a single HTML file that is capable of hydrating for _any_ of your application paths. It can do this because it only renders the `root` route into the HTML file and then determines which child routes to load based on the browser URL during hydration. This means you can use a `loader` on the root route, but not on any other routes because we don't know which routes to load until hydration in the browser.

If you want to pre-render paths with `ssr:false`, those matched routes _can_ have loaders because we'll pre-render all of the matched routes for those paths, not just the root. You cannot include `actions` or `headers` functions in any routes when `ssr:false` is set because there will be no runtime server to run them on.

### Pre-rendering with a SPA Fallback

If you want `ssr:false` but don't want to pre-render _all_ of your routes - that's fine too! You may have some paths where you need the performance/SEO benefits of pre-rendering, but other pages where a SPA would be fine.

You can do this using the combination of config options as well - just limit your `prerender` config to the paths that you want to pre-render and React Router will also output a "SPA Fallback" HTML file that can be served to hydrate any other paths (using the same approach as [SPA Mode](./spa)).

This will be written to one of the following paths:

- `build/client/index.html` - If the `/` path is not pre-rendered
- `build/client/__spa-fallback.html` - If the `/` path is pre-rendered

```ts filename=react-router.config.ts
import type { Config } from "@react-router/dev/config";

export default {
ssr: false,

// SPA fallback will be written to build/client/index.html
prerender: ["/about-us"],

// SPA fallback will be written to build/client/__spa-fallback.html
prerender: ["/", "/about-us"],
} satisfies Config;
```

You can configure your deployment server to serve this file for any path that otherwise would 404. Some hosts do this by default, but others don't. As an example, a host may support a `_redirects` file to do this:

```
# If you did not pre-render the `/` route
/* /index.html 200
# If you pre-rendered the `/` route
/* /__spa-fallback.html 200
```

If you're getting 404s at valid routes for your app, it's likely you need to configure your host.

Here's another example of how you can do this with the [`sirv-cli`](https://www.npmjs.com/package/sirv-cli#user-content-single-page-applications) tool:

```sh
# If you did not pre-render the `/` route
sirv-cli build/client --single index.html

# If you pre-rendered the `/` route
sirv-cli build/client --single __spa-fallback.html
```
86 changes: 67 additions & 19 deletions docs/how-to/spa.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,24 @@ There are two ways to ship a single page app with React Router
- **as a library** - Instead of using React Router's framework features, you can use it as a library in your own SPA architecture. Refer to [React Router as a Library](../start/library/installation) guides.
- **as a framework** - This guide will focus here

## 1. Disable Server Rendering
## Overview

Server rendering is enabled by default. Set the ssr flag to false in `react-router.config.ts` to disable it.
When using React Router as a framework, you can enable "SPA Mode" by setting `ssr:false` in your `react-router.config.ts` file. This will disable runtime server rendering and generate an `index.html` at build time that you can serve and hydrate as a SPA.

Typical Single Page apps send a mostly blank `index.html` template with little more than an empty `<div id="root"></div>`. In contrast, `react-router build` (in SPA Mode) pre-renders your root route at build time into an `index.html` file. This means you can:

- Send more than an empty `<div>`
- Use a root `loader` to load data for your application shell
- Use React components to generate the initial page users see (root `HydrateFallback`)
- Re-enable server rendering later without changing anything about your UI

It's important to note that setting `ssr:false` only disables _runtime server rendering_. React Router will still server render your root route at _build time_ to generate the `index.html` file. This is why your project still needs a dependency on `@react-router/node` and your routes need to be SSR-safe. That means you can't call `window` or other browser-only APIs during the initial render, even when server rendering is disabled.

<docs-info>SPA Mode is a special form of "Pre-Rendering" that allows you to serve all paths in your application from the same HTML file. Please refer to the [Pre-Rendering](./pre-rendering) guide if you want to do more extensive pre-rendering.</docs-info>

## 1. Disable Runtime Server Rendering

Server rendering is enabled by default. Set the `ssr` flag to `false` in `react-router.config.ts` to disable it.

```ts filename=react-router.config.ts lines=[4]
import { type Config } from "@react-router/dev/config";
Expand All @@ -23,7 +38,56 @@ export default {

With this set to false, the server build will no longer be generated.

## 2. Use client loaders and client actions
<docs-info>It's important to note that setting `ssr:false` only disables _runtime server rendering_. React Router will still server render your root route at _build time_ to generate the `index.html` file. This is why your project still needs a dependency on `@react-router/node` and your routes need to be SSR-safe. That means you can't call `window` or other browser-only APIs during the initial render, even when server rendering is disabled.</docs-info>

## 2. Add a `HydrateFallback` and optional `loader` to your root route

SPA Mode will generate an `index.html` file at build-time that you can serve as the entry point for your SPA. This will only render the root route so that it is capable of hydrating at runtime for any path in your application.

To provide a better loading UI than an empty `<div>`, you can add a `HydrateFallback` component to your root route to render your loading UI into the `index.html` at build time. This way, it will be shown to users immediately while the SPA is loading/hydrating.

```tsx filename=root.tsx lines=[7-9]
import LoadingScreen from "./components/loading-screen";

export function Layout() {
return <html>{/*...*/}</html>;
}

export function HydrateFallback() {
return <LoadingScreen />;
}

export default function App() {
return <Outlet />;
}
```

Because the root route is server-rendered at build time, you can also use a `loader` in your root route if you choose. This `loader` will be called at build time ans the data will be available via the optional `HydrateFallback` `loaderData` prop.

```tsx filename=root.tsx lines=[5,10,14]
import { Route } from "./+types/root";

export async function loader() {
return {
version: await getVersion(),
};
}

export function HydrateFallback({
loaderData,
}: Route.ComponentProps) {
return (
<div>
<h1>Loading version {loaderData.version}...</h1>
<AwesomeSpinner />
</div>
);
}
```

You cannot include a `loader` in any other routes in your app when using SPA Mode unless you are [pre-rendering those pages](./pre-rendering).

## 3. Use client loaders and client actions

With server rendering disabled, you can still use `clientLoader` and `clientAction` to manage route data and mutations.

Expand All @@ -45,10 +109,6 @@ export async function clientAction({
}
```

## 3. Pre-rendering

Pre-rendering can be configured for paths with static data known at build time for faster initial page loads. Refer to [Pre-rendering](./pre-rendering) to set it up.

## 4. Direct all URLs to index.html

After running `react-router build`, deploy the `build/client` directory to whatever static host you prefer.
Expand All @@ -60,15 +120,3 @@ Common to deploying any SPA, you'll need to configure your host to direct all UR
```

If you're getting 404s at valid routes for your app, it's likely you need to configure your host.

## Important Note

Typical Single Pages apps send a mostly blank `index.html` template with little more than an empty `<div id="root"></div>`.

In contrast `react-router build` (with server rendering disabled) pre-renders your root and index routes. This means you can:

- Send more than an empty div
- Use React components to generate the initial page users see
- Re-enable server rendering later without changing anything about your UI

React Router will still server render your index route to generate that `index.html` file. This is why your project still needs a dependency on `@react-router/node` and your routes need to be SSR-safe. That means you can't call `window` or other browser-only APIs during the initial render, even when server rendering is disabled.
Loading

0 comments on commit 7f1f4b9

Please sign in to comment.