Skip to content

Commit 0268369

Browse files
committed
Merge branch 'release-next'
2 parents ab67e21 + 4b5793d commit 0268369

File tree

132 files changed

+13112
-5043
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

132 files changed

+13112
-5043
lines changed

CHANGELOG.md

Lines changed: 251 additions & 112 deletions
Large diffs are not rendered by default.

contributors.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
- coryhouse
7272
- ctnelson1997
7373
- cvbuelow
74+
- dadamssg
7475
- damianstasik
7576
- danielberndt
7677
- daniilguit
@@ -247,6 +248,7 @@
247248
- pavsoldatov
248249
- pcattori
249250
- petersendidit
251+
- phryneas
250252
- phildl
251253
- pierophp
252254
- printfn
@@ -314,6 +316,7 @@
314316
- tobias-edwards
315317
- tom-sherman
316318
- tomasr8
319+
- TomerAberbach
317320
- tony-sn
318321
- TooTallNate
319322
- torztomasz

docs/community/api-development-strategy.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ Unstable flags are not recommended for production:
3030

3131
When you opt-in to an unstable flag you are becoming a contributor to the project, rather than a user. We appreciate your help, but please be aware of the new role!
3232

33+
Because unstable flags are experimental and not guaranteed to stick around, we ship them in SemVer patch releases because they're not new _stable_/_documented_ APIs. When an unstable flag stabilizes into a Future Flag, that will be released in a SemVer minor release and will be properly documented and added to the [Future Flags Guide](../upgrading/future).
34+
3335
To learn about current unstable flags, keep an eye on the [CHANGELOG](../start/changelog).
3436

3537
### Example New Feature Flow

docs/explanation/code-splitting.md

Lines changed: 191 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ If the user visits `"/about"` then the bundles for `about.tsx` will be loaded bu
3030

3131
## Removal of Server Code
3232

33-
Any server-only Route Module APIs will be removed from the bundles. Consider this route module:
33+
Any server-only [Route Module APIs][route-module] will be removed from the bundles. Consider this route module:
3434

3535
```tsx
3636
export async function loader() {
@@ -52,3 +52,193 @@ export default function Component({ loaderData }) {
5252
```
5353

5454
After building for the browser, only the `Component` will still be in the bundle, so you can use server-only code in the other module exports.
55+
56+
## Splitting Route Modules
57+
58+
<docs-info>
59+
60+
This feature is only enabled when setting the `unstable_splitRouteModules` future flag:
61+
62+
```tsx filename=react-router-config.ts
63+
export default {
64+
future: {
65+
unstable_splitRouteModules: true,
66+
},
67+
};
68+
```
69+
70+
</docs-info>
71+
72+
One of the conveniences of the [Route Module API][route-module] is that everything a route needs is in a single file. Unfortunately this comes with a performance cost in some cases when using the `clientLoader`, `clientAction`, and `HydrateFallback` APIs.
73+
74+
As a basic example, consider this route module:
75+
76+
```tsx filename=routes/example.tsx
77+
import { MassiveComponent } from "~/components";
78+
79+
export async function clientLoader() {
80+
return await fetch("https://example.com/api").then(
81+
(response) => response.json()
82+
);
83+
}
84+
85+
export default function Component({ loaderData }) {
86+
return <MassiveComponent data={loaderData} />;
87+
}
88+
```
89+
90+
In this example we have a minimal `clientLoader` export that makes a basic fetch call, whereas the default component export is much larger. This is a problem for performance because it means that if we want to navigate to this route client-side, the entire route module must be downloaded before the client loader can start running.
91+
92+
To visualize this as a timeline:
93+
94+
<docs-info>In the following timeline diagrams, different characters are used within the Route Module bars to denote the different Route Module APIs being exported.</docs-info>
95+
96+
```
97+
Get Route Module: |--=======|
98+
Run clientLoader: |-----|
99+
Render: |-|
100+
```
101+
102+
Instead, we want to optimize this to the following:
103+
104+
```
105+
Get clientLoader: |--|
106+
Get Component: |=======|
107+
Run clientLoader: |-----|
108+
Render: |-|
109+
```
110+
111+
To achieve this optimization, React Router will split the route module into multiple smaller modules during the production build process. In this case, we'll end up with two separate [virtual modules][virtual-modules] — one for the client loader and one for the component and its dependencies.
112+
113+
```tsx filename=routes/example.tsx?route-chunk=clientLoader
114+
export async function clientLoader() {
115+
return await fetch("https://example.com/api").then(
116+
(response) => response.json()
117+
);
118+
}
119+
```
120+
121+
```tsx filename=routes/example.tsx?route-chunk=main
122+
import { MassiveComponent } from "~/components";
123+
124+
export default function Component({ loaderData }) {
125+
return <MassiveComponent data={loaderData} />;
126+
}
127+
```
128+
129+
<docs-info>This optimization is automatically applied in framework mode, but you can also implement it in library mode via `route.lazy` and authoring your route in multiple files as covered in our blog post on [lazy loading route modules.][blog-lazy-loading-routes]</docs-info>
130+
131+
Now that these are available as separate modules, the client loader and the component can be downloaded in parallel. This means that the client loader can be executed as soon as it's ready without having to wait for the component.
132+
133+
This optimization is even more pronounced when more Route Module APIs are used. For example, when using `clientLoader`, `clientAction` and `HydrateFallback`, the timeline for a single route module during a client-side navigation might look like this:
134+
135+
```
136+
Get Route Module: |--~~++++=======|
137+
Run clientLoader: |-----|
138+
Render: |-|
139+
```
140+
141+
This would instead be optimized to the following:
142+
143+
```
144+
Get clientLoader: |--|
145+
Get clientAction: |~~|
146+
Get HydrateFallback: SKIPPED
147+
Get Component: |=======|
148+
Run clientLoader: |-----|
149+
Render: |-|
150+
```
151+
152+
Note that this optimization only works when the Route Module APIs being split don't share code within the same file. For example, the following route module can't be split:
153+
154+
```tsx filename=routes/example.tsx
155+
import { MassiveComponent } from "~/components";
156+
157+
const shared = () => console.log("hello");
158+
159+
export async function clientLoader() {
160+
shared();
161+
return await fetch("https://example.com/api").then(
162+
(response) => response.json()
163+
);
164+
}
165+
166+
export default function Component({ loaderData }) {
167+
shared();
168+
return <MassiveComponent data={loaderData} />;
169+
}
170+
```
171+
172+
This route will still work, but since both the client loader and the component depend on the `shared` function defined within the same file, it will be de-optimized into a single route module.
173+
174+
To avoid this, you can extract any code shared between exports into a separate file. For example:
175+
176+
```tsx filename=routes/example/shared.tsx
177+
export const shared = () => console.log("hello");
178+
```
179+
180+
You can then import this shared code in your route module without triggering the de-optimization:
181+
182+
```tsx filename=routes/example/route.tsx
183+
import { MassiveComponent } from "~/components";
184+
import { shared } from "./shared";
185+
186+
export async function clientLoader() {
187+
shared();
188+
return await fetch("https://example.com/api").then(
189+
(response) => response.json()
190+
);
191+
}
192+
193+
export default function Component({ loaderData }) {
194+
shared();
195+
return <MassiveComponent data={loaderData} />;
196+
}
197+
```
198+
199+
Since the shared code is in its own module, React Router is now able to split this route module into two separate virtual modules:
200+
201+
```tsx filename=routes/example/route.tsx?route-chunk=clientLoader
202+
import { shared } from "./shared";
203+
204+
export async function clientLoader() {
205+
shared();
206+
return await fetch("https://example.com/api").then(
207+
(response) => response.json()
208+
);
209+
}
210+
```
211+
212+
```tsx filename=routes/example/route.tsx?route-chunk=main
213+
import { MassiveComponent } from "~/components";
214+
import { shared } from "./shared";
215+
216+
export default function Component({ loaderData }) {
217+
shared();
218+
return <MassiveComponent data={loaderData} />;
219+
}
220+
```
221+
222+
If your project is particularly performance sensitive, you can set the `unstable_splitRouteModules` future flag to `"enforce"`:
223+
224+
```tsx filename=react-router-config.ts
225+
export default {
226+
future: {
227+
unstable_splitRouteModules: "enforce",
228+
},
229+
};
230+
```
231+
232+
This setting will raise an error if any route modules can't be split:
233+
234+
```
235+
Error splitting route module: routes/example/route.tsx
236+
237+
- clientLoader
238+
239+
This export could not be split into its own chunk because it shares code with other exports. You should extract any shared code into its own module and then import it within the route module.
240+
```
241+
242+
[route-module]: ../../start/framework/route-module
243+
[virtual-modules]: https://vite.dev/guide/api-plugin#virtual-modules-convention
244+
[blog-lazy-loading-routes]: https://remix.run/blog/lazy-loading-routes#advanced-usage-and-optimizations

docs/how-to/headers.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export async function loader({ params }: LoaderArgs) {
4747

4848
### 2. Return from `headers` export
4949

50-
Headers from loaders and actions are not sent in a hidden way, you must return them from the `headers` export.
50+
Headers from loaders and actions are not sent automatically. You must explicitly return them from the `headers` export.
5151

5252
```tsx
5353
export function headers({

docs/how-to/pre-rendering.md

Lines changed: 98 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,36 +4,45 @@ title: Pre-Rendering
44

55
# Pre-Rendering
66

7-
Pre-rendering allows you to render pages at build time instead of on a server to speed up pages loads for static content.
7+
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:
88

9-
## Configuration
9+
- Alongside a runtime SSR server ith `ssr:true` (the default value)
10+
- Deployed to a static file server with `ssr:false`
11+
12+
## Pre-rendering with `ssr:true`
13+
14+
### Configuration
1015

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

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

1621
export default {
17-
// all static route paths
18-
// (no dynamic segments like "/post/:slug")
22+
// Can be omitted - defaults to true
23+
ssr: true,
24+
25+
// all static paths (no dynamic segments like "/post/:slug")
1926
prerender: true,
2027

21-
// any url
28+
// specific paths
2229
prerender: ["/", "/blog", "/blog/popular-post"],
2330

2431
// async function for dependencies like a CMS
2532
async prerender({ getStaticPaths }) {
2633
let posts = await fakeGetPostsFromCMS();
27-
return ["/", "/blog"].concat(
28-
posts.map((post) => post.href)
29-
);
34+
return [
35+
"/",
36+
"/blog",
37+
...posts.map((post) => post.href),
38+
];
3039
},
3140
} satisfies Config;
3241
```
3342

34-
## Data Loading and Pre-rendering
43+
### Data Loading and Pre-rendering
3544

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

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

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

53-
## Static File Output
62+
### Static File Output
5463

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

@@ -74,3 +83,80 @@ Prerender: Generated build/client/blog/my-first-post/index.html
7483
```
7584

7685
During development, pre-rendering doesn't save the rendered results to the public directory, this only happens for `react-router build`.
86+
87+
## Pre-rendering with `ssr:false`
88+
89+
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.
90+
91+
To disable runtime SSR and configure pre-rendering to be served from a static file server, you can set the `ssr:false` config flag:
92+
93+
```ts filename=react-router.config.ts
94+
import type { Config } from "@react-router/dev/config";
95+
96+
export default {
97+
ssr: false, // disable runtime server rendering
98+
prerender: true, // pre-render all static routes
99+
} satisfies Config;
100+
```
101+
102+
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.
103+
104+
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.
105+
106+
### Pre-rendering with a SPA Fallback
107+
108+
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.
109+
110+
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)).
111+
112+
This will be written to one of the following paths:
113+
114+
- `build/client/index.html` - If the `/` path is not pre-rendered
115+
- `build/client/__spa-fallback.html` - If the `/` path is pre-rendered
116+
117+
```ts filename=react-router.config.ts
118+
import type { Config } from "@react-router/dev/config";
119+
120+
export default {
121+
ssr: false,
122+
123+
// SPA fallback will be written to build/client/index.html
124+
prerender: ["/about-us"],
125+
126+
// SPA fallback will be written to build/client/__spa-fallback.html
127+
prerender: ["/", "/about-us"],
128+
} satisfies Config;
129+
```
130+
131+
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:
132+
133+
```
134+
# If you did not pre-render the `/` route
135+
/* /index.html 200
136+
137+
# If you pre-rendered the `/` route
138+
/* /__spa-fallback.html 200
139+
```
140+
141+
If you're getting 404s at valid routes for your app, it's likely you need to configure your host.
142+
143+
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:
144+
145+
```sh
146+
# If you did not pre-render the `/` route
147+
sirv-cli build/client --single index.html
148+
149+
# If you pre-rendered the `/` route
150+
sirv-cli build/client --single __spa-fallback.html
151+
```
152+
153+
### Invalid Exports
154+
155+
When pre-rendering with `ssr:false`, React Router will error at build time if you have invalid exports to help prevent some mistakes that can be easily overlooked.
156+
157+
- `headers`/`action` functions are prohibited in all routes because there will be no runtime server on which to run them
158+
- When using `ssr:false` without a `prerender` config (SPA Mode), a `loader` is permitted on the root route only
159+
- When using `ssr:false` with a `prerender` config, a `loader` is permitted on any route matched by a `prerender` path
160+
- If you are using a `loader` on a pre-rendered route that has child routes, you will need to make sure the parent `loaderData` can be determined at run-time properly by either:
161+
- Pre-rendering all child routes so that the parent `loader` can be called at build-time for each child route path and rendered into a `.data` file, or
162+
- Use a `clientLoader` on the parent that can be called at run-time for non-pre-rendered child paths

0 commit comments

Comments
 (0)