-
-
Notifications
You must be signed in to change notification settings - Fork 10.7k
docs: framework convention docs #13888
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
9b9089a
docs: start framework conventions api docs
brookslybrand e56f16e
Adjust order of api docs groups
brookslybrand 181dbb3
cleanup entry.client.tsx and entry.server.tsx
brookslybrand 8eed2c5
Add routes.md example
brookslybrand db500d3
Cleanup root.tsx.md
brookslybrand 74f30e0
Update react-router.config.ts.md
brookslybrand 6c817dc
Simplify special-files doc
brookslybrand 9506fea
Reorder API sections
brookslybrand 0b4092b
Move client modules and server modules to their own files and hide sp…
brookslybrand 35d8fec
Update docs/api/framework-conventions/entry.server.tsx.md
brookslybrand 204f361
cleanup entry.server.tsx
brookslybrand File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
--- | ||
title: Data Routers | ||
order: 4 | ||
--- | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
--- | ||
title: Declarative Routers | ||
order: 5 | ||
--- | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
--- | ||
title: .client modules | ||
--- | ||
|
||
# `.client` modules | ||
|
||
[MODES: framework] | ||
|
||
## Summary | ||
|
||
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. | ||
|
||
```ts filename=feature-check.client.ts | ||
// this would break the server | ||
export const supportsVibrationAPI = | ||
"vibrate" in window.navigator; | ||
``` | ||
|
||
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. | ||
|
||
```ts | ||
import { supportsVibrationAPI } from "./feature-check.client.ts"; | ||
|
||
console.log(supportsVibrationAPI); | ||
// server: undefined | ||
// client: true | false | ||
``` | ||
|
||
## Usage Patterns | ||
|
||
### Individual Files | ||
|
||
Mark individual files as client-only by adding `.client` to the filename: | ||
|
||
```txt | ||
app/ | ||
├── utils.client.ts 👈 client-only file | ||
├── feature-detection.client.ts | ||
└── root.tsx | ||
``` | ||
|
||
### Client Directories | ||
|
||
Mark entire directories as client-only by using `.client` in the directory name: | ||
|
||
```txt | ||
app/ | ||
├── .client/ 👈 entire directory is client-only | ||
│ ├── analytics.ts | ||
│ ├── feature-detection.ts | ||
│ └── browser-utils.ts | ||
├── components/ | ||
└── root.tsx | ||
``` | ||
|
||
## Examples | ||
|
||
### Browser Feature Detection | ||
|
||
```ts filename=app/utils/browser.client.ts | ||
export const canUseDOM = typeof window !== "undefined"; | ||
|
||
export const hasWebGL = !!window.WebGLRenderingContext; | ||
|
||
export const supportsVibrationAPI = | ||
"vibrate" in window.navigator; | ||
``` | ||
|
||
### Client-Only Libraries | ||
|
||
```ts filename=app/analytics.client.ts | ||
// This would break on the server | ||
import { track } from "some-browser-only-analytics-lib"; | ||
|
||
export function trackEvent(eventName: string, data: any) { | ||
track(eventName, data); | ||
} | ||
``` | ||
|
||
### Using Client Modules | ||
|
||
```tsx filename=app/routes/dashboard.tsx | ||
import { useEffect } from "react"; | ||
import { | ||
canUseDOM, | ||
supportsLocalStorage, | ||
supportsVibrationAPI, | ||
} from "../utils/browser.client.ts"; | ||
import { trackEvent } from "../analytics.client.ts"; | ||
|
||
export default function Dashboard() { | ||
useEffect(() => { | ||
// These values are undefined on the server | ||
if (canUseDOM && supportsVibrationAPI) { | ||
console.log("Device supports vibration"); | ||
} | ||
|
||
// Safe localStorage usage | ||
const savedTheme = | ||
supportsLocalStorage.getItem("theme"); | ||
if (savedTheme) { | ||
document.body.className = savedTheme; | ||
} | ||
|
||
trackEvent("dashboard_viewed", { | ||
timestamp: Date.now(), | ||
}); | ||
}, []); | ||
|
||
return <div>Dashboard</div>; | ||
} | ||
``` | ||
|
||
[use_effect]: https://react.dev/reference/react/useEffect |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
--- | ||
title: entry.client.tsx | ||
order: 4 | ||
--- | ||
|
||
# entry.client.tsx | ||
|
||
[MODES: framework] | ||
|
||
## Summary | ||
|
||
<docs-info> | ||
This file is optional | ||
</docs-info> | ||
|
||
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] | ||
|
||
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. | ||
|
||
```tsx filename=app/entry.client.tsx | ||
import { startTransition, StrictMode } from "react"; | ||
import { hydrateRoot } from "react-dom/client"; | ||
import { HydratedRouter } from "react-router/dom"; | ||
|
||
startTransition(() => { | ||
hydrateRoot( | ||
document, | ||
<StrictMode> | ||
<HydratedRouter /> | ||
</StrictMode> | ||
); | ||
}); | ||
``` | ||
|
||
## Generating `entry.client.tsx` | ||
|
||
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: | ||
|
||
```shellscript nonumber | ||
npx react-router reveal | ||
``` | ||
|
||
[server-entry]: ./entry.server.tsx |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
--- | ||
title: entry.server.tsx | ||
order: 5 | ||
--- | ||
|
||
# entry.server.tsx | ||
|
||
[MODES: framework] | ||
|
||
## Summary | ||
|
||
<docs-info> | ||
This file is optional | ||
</docs-info> | ||
|
||
This file is the server-side entry point that controls how your React Router application generates HTTP responses on the server. | ||
|
||
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]. | ||
|
||
## Generating `entry.server.tsx` | ||
|
||
By default, React Router will handle generating the HTTP Response for you. You can reveal the default entry server file with the following: | ||
|
||
```shellscript nonumber | ||
npx react-router reveal | ||
``` | ||
|
||
## Exports | ||
|
||
### `default` | ||
|
||
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. | ||
|
||
```tsx filename=app/entry.server.tsx | ||
import { PassThrough } from "node:stream"; | ||
import type { EntryContext } from "react-router"; | ||
import { createReadableStreamFromReadable } from "@react-router/node"; | ||
import { ServerRouter } from "react-router"; | ||
import { renderToPipeableStream } from "react-dom/server"; | ||
|
||
export default function handleRequest( | ||
request: Request, | ||
responseStatusCode: number, | ||
responseHeaders: Headers, | ||
routerContext: EntryContext | ||
) { | ||
return new Promise((resolve, reject) => { | ||
const { pipe, abort } = renderToPipeableStream( | ||
<ServerRouter | ||
context={routerContext} | ||
url={request.url} | ||
/>, | ||
{ | ||
onShellReady() { | ||
responseHeaders.set("Content-Type", "text/html"); | ||
|
||
const body = new PassThrough(); | ||
const stream = | ||
createReadableStreamFromReadable(body); | ||
|
||
resolve( | ||
new Response(stream, { | ||
headers: responseHeaders, | ||
status: responseStatusCode, | ||
}) | ||
); | ||
|
||
pipe(body); | ||
}, | ||
onShellError(error: unknown) { | ||
reject(error); | ||
}, | ||
} | ||
); | ||
}); | ||
} | ||
``` | ||
|
||
### `streamTimeout` | ||
|
||
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. | ||
|
||
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`. | ||
|
||
```tsx lines=[1-2,13-15] | ||
// Reject all pending promises from handler functions after 10 seconds | ||
export const streamTimeout = 10000; | ||
|
||
export default function handleRequest(...) { | ||
return new Promise((resolve, reject) => { | ||
// ... | ||
|
||
const { pipe, abort } = renderToPipeableStream( | ||
<ServerRouter context={routerContext} url={request.url} />, | ||
{ /* ... */ } | ||
); | ||
|
||
// Abort the streaming render pass after 11 seconds to allow the rejected | ||
// boundaries to be flushed | ||
setTimeout(abort, streamTimeout + 1000); | ||
}); | ||
} | ||
``` | ||
|
||
### `handleDataRequest` | ||
|
||
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. | ||
|
||
```tsx | ||
export function handleDataRequest( | ||
response: Response, | ||
{ | ||
request, | ||
params, | ||
context, | ||
}: LoaderFunctionArgs | ActionFunctionArgs | ||
) { | ||
response.headers.set("X-Custom-Header", "value"); | ||
return response; | ||
} | ||
``` | ||
|
||
### `handleError` | ||
|
||
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). | ||
|
||
```tsx | ||
export function handleError( | ||
error: unknown, | ||
{ | ||
request, | ||
params, | ||
context, | ||
}: LoaderFunctionArgs | ActionFunctionArgs | ||
) { | ||
if (!request.signal.aborted) { | ||
sendErrorToErrorReportingService(error); | ||
console.error(formatErrorForJsonLogging(error)); | ||
} | ||
} | ||
``` | ||
|
||
_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._ | ||
|
||
**Streaming Rendering Errors** | ||
|
||
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. | ||
|
||
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 | ||
|
||
For an example, please refer to the default [`entry.server.tsx`][node-streaming-entry-server] for Node. | ||
|
||
**Thrown Responses** | ||
|
||
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. | ||
|
||
[client-entry]: ./entry.client.tsx | ||
[serverrouter]: ../components/ServerRouter | ||
[streaming]: ../how-to/suspense | ||
[rendertopipeablestream]: https://react.dev/reference/react-dom/server/renderToPipeableStream | ||
[rendertoreadablestream]: https://react.dev/reference/react-dom/server/renderToReadableStream | ||
[node-streaming-entry-server]: https://github.com/remix-run/react-router/blob/dev/packages/react-router-dev/config/defaults/entry.server.node.tsx |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
--- | ||
title: Framework Conventions | ||
order: 3 | ||
--- |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.