Skip to content

Commit dedfa72

Browse files
committed
Merge branch 'main' into release-next
2 parents 6f2168e + b4a37a8 commit dedfa72

File tree

19 files changed

+292
-25
lines changed

19 files changed

+292
-25
lines changed

contributors.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@
4747
- bhbs
4848
- bilalk711
4949
- bobziroll
50+
- bravo-kernel
5051
- Brendonovich
52+
- briankb
5153
- BrianT1414
5254
- brockross
5355
- brookslybrand
@@ -241,6 +243,7 @@
241243
- printfn
242244
- promet99
243245
- proshunsuke
246+
- pwdcd
244247
- pyitphyoaung
245248
- refusado
246249
- reyronald
@@ -313,6 +316,7 @@
313316
- valerii15298
314317
- ValiantCat
315318
- vdusart
319+
- VictorElHajj
316320
- vijaypushkin
317321
- vikingviolinist
318322
- vishwast03

decisions/0012-type-inference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ This was an additional indication that maybe a TypeScript plugin was not the rig
278278

279279
## Summary
280280

281-
By leaning into automated typegen within a TypeScript plugin, we radically simplify React Router's runtime APIs while providing strong type inference across the entire framework.
281+
By leaning into automated typegen, we radically simplify React Router's runtime APIs while providing strong type inference across the entire framework.
282282
We can continue to support programmatic routing _and_ file-based routing in `routes.ts` while providing typesafety with the same approach and same code path.
283283
We can design our runtime APIs without introducing bespoke ways to inform TypeScript of the route hierarchy.
284284

docs/explanation/special-files.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,7 @@ export default function handleRequest(...) {
248248
{ /* ... */ }
249249
);
250250

251-
// Abort the streaming render pass after 11 seconds soto allow the rejected
251+
// Abort the streaming render pass after 11 seconds so to allow the rejected
252252
// boundaries to be flushed
253253
setTimeout(abort, streamTimeout + 1000);
254254
});

docs/how-to/client-data.md

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
---
2+
title: Client Data
3+
---
4+
5+
# Client Data
6+
7+
You can fetch and mutate data directly in the browser using `clientLoader` and `clientAction` functions.
8+
9+
These functions are the primary mechanism for data handling when using [SPA mode][spa]. This guide demonstrates common use cases for leveraging client data in Server-Side Rendering (SSR).
10+
11+
## Skip the Server Hop
12+
13+
When using React Router with a Backend-For-Frontend (BFF) architecture, you might want to bypass the React Router server and communicate directly with your backend API. This approach requires proper authentication handling and assumes no CORS restrictions. Here's how to implement this:
14+
15+
1. Load the data from server `loader` on the document load
16+
2. Load the data from the `clientLoader` on all subsequent loads
17+
18+
In this scenario, React Router will _not_ call the `clientLoader` on hydration - and will only call it on subsequent navigations.
19+
20+
```tsx lines=[4,11]
21+
export async function loader({
22+
request,
23+
}: Route.LoaderArgs) {
24+
const data = await fetchApiFromServer({ request }); // (1)
25+
return data;
26+
}
27+
28+
export async function clientLoader({
29+
request,
30+
}: Route.ClientLoaderArgs) {
31+
const data = await fetchApiFromClient({ request }); // (2)
32+
return data;
33+
}
34+
```
35+
36+
## Fullstack State
37+
38+
Sometimes you need to combine data from both the server and browser (like IndexedDB or browser SDKs) before rendering a component. Here's how to implement this pattern:
39+
40+
1. Load the partial data from server `loader` on the document load
41+
2. Export a [`HydrateFallback`][hydratefallback] component to render during SSR because we don't yet have a full set of data
42+
3. Set `clientLoader.hydrate = true`, this instructs React Router to call the clientLoader as part of initial document hydration
43+
4. Combine the server data with the client data in `clientLoader`
44+
45+
```tsx lines=[4-6,19-20,23,26]
46+
export async function loader({
47+
request,
48+
}: Route.LoaderArgs) {
49+
const partialData = await getPartialDataFromDb({
50+
request,
51+
}); // (1)
52+
return partialData;
53+
}
54+
55+
export async function clientLoader({
56+
request,
57+
serverLoader,
58+
}: Route.ClientLoaderArgs) {
59+
const [serverData, clientData] = await Promise.all([
60+
serverLoader(),
61+
getClientData(request),
62+
]);
63+
return {
64+
...serverData, // (4)
65+
...clientData, // (4)
66+
};
67+
}
68+
clientLoader.hydrate = true as const; // (3)
69+
70+
export function HydrateFallback() {
71+
return <p>Skeleton rendered during SSR</p>; // (2)
72+
}
73+
74+
export default function Component({
75+
// This will always be the combined set of server + client data
76+
loaderData,
77+
}: Route.ComponentProps) {
78+
return <>...</>;
79+
}
80+
```
81+
82+
## Choosing Server or Client Data Loading
83+
84+
You can mix data loading strategies across your application, choosing between server-only or client-only data loading for each route. Here's how to implement both approaches:
85+
86+
1. Export a `loader` when you want to use server data
87+
2. Export `clientLoader` and a `HydrateFallback` when you want to use client data
88+
89+
A route that only depends on a server loader looks like this:
90+
91+
```tsx filename=app/routes/server-data-route.tsx
92+
export async function loader({
93+
request,
94+
}: Route.LoaderArgs) {
95+
const data = await getServerData(request);
96+
return data;
97+
}
98+
99+
export default function Component({
100+
loaderData, // (1) - server data
101+
}: Route.ComponentProps) {
102+
return <>...</>;
103+
}
104+
```
105+
106+
A route that only depends on a client loader looks like this.
107+
108+
```tsx filename=app/routes/client-data-route.tsx
109+
export async function clientLoader({
110+
request,
111+
}: Route.ClientLoaderArgs) {
112+
const clientData = await getClientData(request);
113+
return clientData;
114+
}
115+
// Note: you do not have to set this explicitly - it is implied if there is no `loader`
116+
clientLoader.hydrate = true;
117+
118+
// (2)
119+
export function HydrateFallback() {
120+
return <p>Skeleton rendered during SSR</p>;
121+
}
122+
123+
export default function Component({
124+
loaderData, // (2) - client data
125+
}: Route.ComponentProps) {
126+
return <>...</>;
127+
}
128+
```
129+
130+
## Client-Side Caching
131+
132+
You can implement client-side caching (using memory, localStorage, etc.) to optimize server requests. Here's a pattern that demonstrates cache management:
133+
134+
1. Load the data from server `loader` on the document load
135+
2. Set `clientLoader.hydrate = true` to prime the cache
136+
3. Load subsequent navigations from the cache via `clientLoader`
137+
4. Invalidate the cache in your `clientAction`
138+
139+
Note that since we are not exporting a `HydrateFallback` component, we will SSR the route component and then run the `clientLoader` on hydration, so it's important that your `loader` and `clientLoader` return the same data on initial load to avoid hydration errors.
140+
141+
```tsx lines=[4,26,32,39,46]
142+
export async function loader({
143+
request,
144+
}: Route.LoaderArgs) {
145+
const data = await getDataFromDb({ request }); // (1)
146+
return data;
147+
}
148+
149+
export async function action({
150+
request,
151+
}: Route.ActionArgs) {
152+
await saveDataToDb({ request });
153+
return { ok: true };
154+
}
155+
156+
let isInitialRequest = true;
157+
158+
export async function clientLoader({
159+
request,
160+
serverLoader,
161+
}: Route.ClientLoaderArgs) {
162+
const cacheKey = generateKey(request);
163+
164+
if (isInitialRequest) {
165+
isInitialRequest = false;
166+
const serverData = await serverLoader();
167+
cache.set(cacheKey, serverData); // (2)
168+
return serverData;
169+
}
170+
171+
const cachedData = await cache.get(cacheKey);
172+
if (cachedData) {
173+
return cachedData; // (3)
174+
}
175+
176+
const serverData = await serverLoader();
177+
cache.set(cacheKey, serverData);
178+
return serverData;
179+
}
180+
clientLoader.hydrate = true; // (2)
181+
182+
export async function clientAction({
183+
request,
184+
serverAction,
185+
}: Route.ClientActionArgs) {
186+
const cacheKey = generateKey(request);
187+
cache.delete(cacheKey); // (4)
188+
const serverData = await serverAction();
189+
return serverData;
190+
}
191+
```
192+
193+
[spa]: ../how-to/spa
194+
[hydratefallback]: ../start/framework/route-module#hydratefallback

docs/how-to/file-route-conventions.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ import { flatRoutes } from "@react-router/fs-routes";
2323
export default flatRoutes() satisfies RouteConfig;
2424
```
2525

26+
Any modules in the `app/routes` directory will become routes in your application by default.
27+
The `ignoredRouteFiles` option allows you to specify files that should not be included as routes:
28+
29+
```tsx filename=app/routes.ts
30+
import { type RouteConfig } from "@react-router/dev/routes";
31+
import { flatRoutes } from "@react-router/fs-routes";
32+
33+
export default flatRoutes({
34+
ignoredRouteFiles: ["home.tsx"],
35+
}) satisfies RouteConfig;
36+
```
37+
2638
This will look for routes in the `app/routes` directory by default, but this can be configured via the `rootDirectory` option which is relative to your app directory:
2739

2840
```tsx filename=app/routes.ts
@@ -38,7 +50,7 @@ The rest of this guide will assume you're using the default `app/routes` directo
3850

3951
## Basic Routes
4052

41-
Any modules in the `app/routes` directory will become routes in your application. The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index_route] for the [root route][root_route]. You can use `.js`, `.jsx`, `.ts` or `.tsx` file extensions.
53+
The filename maps to the route's URL pathname, except for `_index.tsx` which is the [index route][index_route] for the [root route][root_route]. You can use `.js`, `.jsx`, `.ts` or `.tsx` file extensions.
4254

4355
```text lines=[3-4]
4456
app/

docs/how-to/headers.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ export function headers({
5858
}
5959
```
6060

61+
One notable exception is `Set-Cookie` headers, which are automatically preserved from `headers`, `loader`, and `action` in parent routes, even without exporting `headers` from the child route.
62+
6163
## Merging with parent headers
6264

6365
Consider these nested routes

docs/how-to/spa.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ If you're getting 404s at valid routes for your app, it's likely you need to con
6363

6464
## Important Note
6565

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

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

docs/start/framework/data-loading.md

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ Data is provided to the route component from `loader` and `clientLoader`.
99

1010
Loader data is automatically serialized from loaders and deserialized in components. In addition to primitive values like strings and numbers, loaders can return promises, maps, sets, dates and more.
1111

12+
The type for the `loaderData` prop is [automatically generated][type-safety].
13+
1214
## Client Data Loading
1315

1416
`clientLoader` is used to fetch data on the client. This is useful for pages or full projects that you'd prefer to fetch data from the browser only.
@@ -25,6 +27,11 @@ export async function clientLoader({
2527
return product;
2628
}
2729

30+
// HydrateFallback is rendered while the client loader is running
31+
export function HydrateFallback() {
32+
return <div>Loading...</div>;
33+
}
34+
2835
export default function Product({
2936
loaderData,
3037
}: Route.ComponentProps) {
@@ -124,10 +131,11 @@ export async function loader({ params }: Route.LoaderArgs) {
124131
}
125132

126133
export async function clientLoader({
134+
serverLoader,
127135
params,
128-
}: Route.ClientLoader) {
136+
}: Route.ClientLoaderArgs) {
129137
const res = await fetch(`/api/products/${params.pid}`);
130-
return res.json();
138+
return { ...serverData, ...res.json() };
131139
}
132140

133141
export default function Product({
@@ -144,13 +152,39 @@ export default function Product({
144152
}
145153
```
146154

155+
You can also force the client loader to run during hydration and before the page renders by setting the `hydrate` property on the function. In this situation you will want to render a `HydrateFallback` component to show a fallback UI while the client loader runs.
156+
157+
```tsx filename=app/product.tsx
158+
export async function loader() {
159+
/* ... */
160+
}
161+
162+
export async function clientLoader() {
163+
/* ... */
164+
}
165+
166+
// force the client loader to run during hydration
167+
clientLoader.hydrate = true as const; // `as const` for type inference
168+
169+
export function HydrateFallback() {
170+
return <div>Loading...</div>;
171+
}
172+
173+
export default function Product() {
174+
/* ... */
175+
}
176+
```
177+
147178
---
148179

149180
Next: [Actions](./actions)
150181

151182
See also:
152183

153184
- [Streaming with Suspense](../../how-to/suspense)
185+
- [Client Data](../../how-to/client-data)
186+
- [Using Fetchers](../../how-to/fetchers#loading-data)
154187

155188
[advanced_data_fetching]: ../tutorials/advanced-data-fetching
156189
[data]: ../../api/react-router/data
190+
[type-safety]: ../../explanation/type-safety

docs/start/framework/routing.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,18 @@ export default [
4949
] satisfies RouteConfig;
5050
```
5151

52-
If you prefer to define your routes via file naming conventions rather than configuration, the `@react-router/fs-routes` package provides a [file system routing convention.][file-route-conventions]
52+
If you prefer to define your routes via file naming conventions rather than configuration, the `@react-router/fs-routes` package provides a [file system routing convention][file-route-conventions]. You can even combine different routing conventions if you like:
53+
54+
```ts filename=app/routes.ts
55+
import { type RouteConfig, route } from "@react-router/dev/routes";
56+
import { flatRoutes } from "@react-router/fs-routes";
57+
58+
export default = [
59+
route("/", "./home.tsx"),
60+
61+
...await flatRoutes(),
62+
] satisfies RouteConfig;
63+
```
5364

5465
## Route Modules
5566

@@ -263,6 +274,8 @@ async function loader({ params }: LoaderArgs) {
263274
}
264275
```
265276

277+
You should ensure that all dynamic segments in a given path are unique. Otherwise, as the `params` object is populated - latter dynamic segment values will override earlier values.
278+
266279
## Optional Segments
267280

268281
You can make a route segment optional by adding a `?` to the end of the segment.

docs/start/library/routing.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ export default function Team() {
168168
}
169169
```
170170

171+
You should ensure that all dynamic segments in a given path are unique. Otherwise, as the `params` object is populated - latter dynamic segment values will override earlier values.
172+
171173
## Optional Segments
172174

173175
You can make a route segment optional by adding a `?` to the end of the segment.

0 commit comments

Comments
 (0)