Skip to content

Commit 4ce55d5

Browse files
docs: framework convention docs (#13888)
* docs: start framework conventions api docs * Adjust order of api docs groups * cleanup entry.client.tsx and entry.server.tsx * Add routes.md example * Cleanup root.tsx.md * Update react-router.config.ts.md * Simplify special-files doc * Reorder API sections * Move client modules and server modules to their own files and hide special files * Update docs/api/framework-conventions/entry.server.tsx.md Co-authored-by: Michaël De Boey <[email protected]> * cleanup entry.server.tsx --------- Co-authored-by: Michaël De Boey <[email protected]>
1 parent f0a66f7 commit 4ce55d5

File tree

12 files changed

+926
-354
lines changed

12 files changed

+926
-354
lines changed

docs/api/components/ServerRouter.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ title: ServerRouter
1010

1111
[Reference Documentation ↗](https://api.reactrouter.com/v7/functions/react_router.ServerRouter.html)
1212

13-
Rendered at the top of the app in a custom entry.server.tsx.
13+
Rendered at the top of the app in a custom [entry.server.tsx][entry-server].
1414

1515
## Props
1616

@@ -31,3 +31,5 @@ _No documentation_
3131
[modes: framework]
3232

3333
_No documentation_
34+
35+
[entry-server]: ../framework-conventions/entry.server.tsx

docs/api/data-routers/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
---
22
title: Data Routers
3+
order: 4
34
---
4-

docs/api/declarative-routers/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
---
22
title: Declarative Routers
3+
order: 5
34
---
4-
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
---
2+
title: .client modules
3+
---
4+
5+
# `.client` modules
6+
7+
[MODES: framework]
8+
9+
## Summary
10+
11+
You may have a file or dependency that uses module side effects in the browser. You can use `*.client.ts` on file names or nest files within `.client` directories to force them out of server bundles.
12+
13+
```ts filename=feature-check.client.ts
14+
// this would break the server
15+
export const supportsVibrationAPI =
16+
"vibrate" in window.navigator;
17+
```
18+
19+
Note that values exported from this module will all be `undefined` on the server, so the only places to use them are in [`useEffect`][use_effect] and user events like click handlers.
20+
21+
```ts
22+
import { supportsVibrationAPI } from "./feature-check.client.ts";
23+
24+
console.log(supportsVibrationAPI);
25+
// server: undefined
26+
// client: true | false
27+
```
28+
29+
## Usage Patterns
30+
31+
### Individual Files
32+
33+
Mark individual files as client-only by adding `.client` to the filename:
34+
35+
```txt
36+
app/
37+
├── utils.client.ts 👈 client-only file
38+
├── feature-detection.client.ts
39+
└── root.tsx
40+
```
41+
42+
### Client Directories
43+
44+
Mark entire directories as client-only by using `.client` in the directory name:
45+
46+
```txt
47+
app/
48+
├── .client/ 👈 entire directory is client-only
49+
│ ├── analytics.ts
50+
│ ├── feature-detection.ts
51+
│ └── browser-utils.ts
52+
├── components/
53+
└── root.tsx
54+
```
55+
56+
## Examples
57+
58+
### Browser Feature Detection
59+
60+
```ts filename=app/utils/browser.client.ts
61+
export const canUseDOM = typeof window !== "undefined";
62+
63+
export const hasWebGL = !!window.WebGLRenderingContext;
64+
65+
export const supportsVibrationAPI =
66+
"vibrate" in window.navigator;
67+
```
68+
69+
### Client-Only Libraries
70+
71+
```ts filename=app/analytics.client.ts
72+
// This would break on the server
73+
import { track } from "some-browser-only-analytics-lib";
74+
75+
export function trackEvent(eventName: string, data: any) {
76+
track(eventName, data);
77+
}
78+
```
79+
80+
### Using Client Modules
81+
82+
```tsx filename=app/routes/dashboard.tsx
83+
import { useEffect } from "react";
84+
import {
85+
canUseDOM,
86+
supportsLocalStorage,
87+
supportsVibrationAPI,
88+
} from "../utils/browser.client.ts";
89+
import { trackEvent } from "../analytics.client.ts";
90+
91+
export default function Dashboard() {
92+
useEffect(() => {
93+
// These values are undefined on the server
94+
if (canUseDOM && supportsVibrationAPI) {
95+
console.log("Device supports vibration");
96+
}
97+
98+
// Safe localStorage usage
99+
const savedTheme =
100+
supportsLocalStorage.getItem("theme");
101+
if (savedTheme) {
102+
document.body.className = savedTheme;
103+
}
104+
105+
trackEvent("dashboard_viewed", {
106+
timestamp: Date.now(),
107+
});
108+
}, []);
109+
110+
return <div>Dashboard</div>;
111+
}
112+
```
113+
114+
[use_effect]: https://react.dev/reference/react/useEffect
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
---
2+
title: entry.client.tsx
3+
order: 4
4+
---
5+
6+
# entry.client.tsx
7+
8+
[MODES: framework]
9+
10+
## Summary
11+
12+
<docs-info>
13+
This file is optional
14+
</docs-info>
15+
16+
This file is the entry point for the browser and is responsible for hydrating the markup generated by the server in your [server entry module][server-entry]
17+
18+
This is the first piece of code that runs in the browser. You can initialize any other client-side code here, such as client side libraries, add client only providers, etc.
19+
20+
```tsx filename=app/entry.client.tsx
21+
import { startTransition, StrictMode } from "react";
22+
import { hydrateRoot } from "react-dom/client";
23+
import { HydratedRouter } from "react-router/dom";
24+
25+
startTransition(() => {
26+
hydrateRoot(
27+
document,
28+
<StrictMode>
29+
<HydratedRouter />
30+
</StrictMode>
31+
);
32+
});
33+
```
34+
35+
## Generating `entry.client.tsx`
36+
37+
By default, React Router will handle hydrating your app on the client for you. You can reveal the default entry client file with the following:
38+
39+
```shellscript nonumber
40+
npx react-router reveal
41+
```
42+
43+
[server-entry]: ./entry.server.tsx
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
---
2+
title: entry.server.tsx
3+
order: 5
4+
---
5+
6+
# entry.server.tsx
7+
8+
[MODES: framework]
9+
10+
## Summary
11+
12+
<docs-info>
13+
This file is optional
14+
</docs-info>
15+
16+
This file is the server-side entry point that controls how your React Router application generates HTTP responses on the server.
17+
18+
This module should render the markup for the current page using a [`<ServerRouter>`][serverrouter] element with the `context` and `url` for the current request. This markup will (optionally) be re-hydrated once JavaScript loads in the browser using the [client entry module][client-entry].
19+
20+
## Generating `entry.server.tsx`
21+
22+
By default, React Router will handle generating the HTTP Response for you. You can reveal the default entry server file with the following:
23+
24+
```shellscript nonumber
25+
npx react-router reveal
26+
```
27+
28+
## Exports
29+
30+
### `default`
31+
32+
The `default` export of this module is a function that lets you create the response, including HTTP status, headers, and HTML, giving you full control over the way the markup is generated and sent to the client.
33+
34+
```tsx filename=app/entry.server.tsx
35+
import { PassThrough } from "node:stream";
36+
import type { EntryContext } from "react-router";
37+
import { createReadableStreamFromReadable } from "@react-router/node";
38+
import { ServerRouter } from "react-router";
39+
import { renderToPipeableStream } from "react-dom/server";
40+
41+
export default function handleRequest(
42+
request: Request,
43+
responseStatusCode: number,
44+
responseHeaders: Headers,
45+
routerContext: EntryContext
46+
) {
47+
return new Promise((resolve, reject) => {
48+
const { pipe, abort } = renderToPipeableStream(
49+
<ServerRouter
50+
context={routerContext}
51+
url={request.url}
52+
/>,
53+
{
54+
onShellReady() {
55+
responseHeaders.set("Content-Type", "text/html");
56+
57+
const body = new PassThrough();
58+
const stream =
59+
createReadableStreamFromReadable(body);
60+
61+
resolve(
62+
new Response(stream, {
63+
headers: responseHeaders,
64+
status: responseStatusCode,
65+
})
66+
);
67+
68+
pipe(body);
69+
},
70+
onShellError(error: unknown) {
71+
reject(error);
72+
},
73+
}
74+
);
75+
});
76+
}
77+
```
78+
79+
### `streamTimeout`
80+
81+
If you are [streaming] responses, you can export an optional `streamTimeout` value (in milliseconds) that will control the amount of time the server will wait for streamed promises to settle before rejecting outstanding promises and closing the stream.
82+
83+
It's recommended to decouple this value from the timeout in which you abort the React renderer. You should always set the React rendering timeout to a higher value so it has time to stream down the underlying rejections from your `streamTimeout`.
84+
85+
```tsx lines=[1-2,13-15]
86+
// Reject all pending promises from handler functions after 10 seconds
87+
export const streamTimeout = 10000;
88+
89+
export default function handleRequest(...) {
90+
return new Promise((resolve, reject) => {
91+
// ...
92+
93+
const { pipe, abort } = renderToPipeableStream(
94+
<ServerRouter context={routerContext} url={request.url} />,
95+
{ /* ... */ }
96+
);
97+
98+
// Abort the streaming render pass after 11 seconds to allow the rejected
99+
// boundaries to be flushed
100+
setTimeout(abort, streamTimeout + 1000);
101+
});
102+
}
103+
```
104+
105+
### `handleDataRequest`
106+
107+
You can export an optional `handleDataRequest` function that will allow you to modify the response of a data request. These are the requests that do not render HTML, but rather return the `loader` and `action` data to the browser once client-side hydration has occurred.
108+
109+
```tsx
110+
export function handleDataRequest(
111+
response: Response,
112+
{
113+
request,
114+
params,
115+
context,
116+
}: LoaderFunctionArgs | ActionFunctionArgs
117+
) {
118+
response.headers.set("X-Custom-Header", "value");
119+
return response;
120+
}
121+
```
122+
123+
### `handleError`
124+
125+
By default, React Router will log encountered server-side errors to the console. If you'd like more control over the logging, or would like to also report these errors to an external service, then you can export an optional `handleError` function which will give you control (and will disable the built-in error logging).
126+
127+
```tsx
128+
export function handleError(
129+
error: unknown,
130+
{
131+
request,
132+
params,
133+
context,
134+
}: LoaderFunctionArgs | ActionFunctionArgs
135+
) {
136+
if (!request.signal.aborted) {
137+
sendErrorToErrorReportingService(error);
138+
console.error(formatErrorForJsonLogging(error));
139+
}
140+
}
141+
```
142+
143+
_Note that you generally want to avoid logging when the request was aborted, since React Router's cancellation and race-condition handling can cause a lot of requests to be aborted._
144+
145+
**Streaming Rendering Errors**
146+
147+
When you are streaming your HTML responses via [`renderToPipeableStream`][rendertopipeablestream] or [`renderToReadableStream`][rendertoreadablestream], your own `handleError` implementation will only handle errors encountered during the initial shell render. If you encounter a rendering error during subsequent streamed rendering you will need to handle these errors manually since the React Router server has already sent the Response by that point.
148+
149+
For `renderToPipeableStream`, you can handle these errors in the `onError` callback function. You will need to toggle a boolean in `onShellReady` so you know if the error was a shell rendering error (and can be ignored) or an async
150+
151+
For an example, please refer to the default [`entry.server.tsx`][node-streaming-entry-server] for Node.
152+
153+
**Thrown Responses**
154+
155+
Note that this does not handle thrown `Response` instances from your `loader`/`action` functions. The intention of this handler is to find bugs in your code which result in unexpected thrown errors. If you are detecting a scenario and throwing a 401/404/etc. `Response` in your `loader`/`action` then it's an expected flow that is handled by your code. If you also wish to log, or send those to an external service, that should be done at the time you throw the response.
156+
157+
[client-entry]: ./entry.client.tsx
158+
[serverrouter]: ../components/ServerRouter
159+
[streaming]: ../how-to/suspense
160+
[rendertopipeablestream]: https://react.dev/reference/react-dom/server/renderToPipeableStream
161+
[rendertoreadablestream]: https://react.dev/reference/react-dom/server/renderToReadableStream
162+
[node-streaming-entry-server]: https://github.com/remix-run/react-router/blob/dev/packages/react-router-dev/config/defaults/entry.server.node.tsx
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
title: Framework Conventions
3+
order: 3
4+
---

0 commit comments

Comments
 (0)