INDIE UNPKG

9.59 kBMarkdownView Raw
1---
2title: Custom Fetch
3unstable: true
4---
5
6# Custom Fetch
7
8[MODES: framework]
9
10<docs-warning>This feature is experimental and may change in minor or patch releases. It is not supported in RSC Framework Mode.</docs-warning>
11
12The `unstable_fetch` prop on [`HydratedRouter`](../api/framework-routers/HydratedRouter) lets you customize the JavaScript-issued requests React Router makes for server loader/action data and lazy route discovery. Use it to add headers, customize fetch options, or apply a request policy based on the operation that initiated the request.
13
14This is a Framework Mode API. In Data Mode, you control fetching in your loaders/actions or through a [`dataStrategy`](./data-strategy#request-metadata-unstable).
15
16## Setup
17
18If you don't have an [`app/entry.client.tsx`](../api/framework-conventions/entry.client.tsx), reveal the default entry:
19
20```shellscript nonumber
21npx react-router reveal entry.client
22```
23
24Pass a custom implementation to `HydratedRouter`:
25
26```tsx filename=app/entry.client.tsx
27import { startTransition, StrictMode } from "react";
28import { hydrateRoot } from "react-dom/client";
29import {
30 HydratedRouter,
31 type unstable_RouterFetch,
32} from "react-router/dom";
33
34const customFetch: unstable_RouterFetch = (
35 request,
36 context,
37) => {
38 console.log(context.type, request.method, request.url);
39 return fetch(request);
40};
41
42startTransition(() => {
43 hydrateRoot(
44 document,
45 <StrictMode>
46 <HydratedRouter unstable_fetch={customFetch} />
47 </StrictMode>,
48 );
49});
50```
51
52The callback receives a constructed `Request`, including its URL, headers, body, and abort signal, and must return a `Promise<Response>`. Pass that request to the browser's `fetch` to preserve those settings, including cancellation.
53
54`HydratedRouter` creates a singleton router and reads `unstable_fetch` only during initialization. Changing the prop later has no effect. If your implementation needs changing values such as authentication tokens, read current application state inside the callback instead of capturing values from a component render.
55
56## Request Context
57
58The [`unstable_RouterFetchContext`](https://api.reactrouter.com/v8/types/react-router.dom.unstable_RouterFetchContext.html) type is exported from `react-router/dom` and has the following shape:
59
60```ts
61import type { NavigationType } from "react-router";
62
63type unstable_RouterFetchContext =
64 | { type: "manifest" }
65 | {
66 type: "navigation";
67 fetcherKey: string | null;
68 navigationType: NavigationType;
69 }
70 | {
71 type: "initialization" | "fetcher" | "revalidation";
72 fetcherKey: string | null;
73 navigationType: null;
74 };
75```
76
77`type` identifies the operation that initiated the request:
78
79- `"manifest"`: Lazy route discovery requests. These have no `fetcherKey` or `navigationType`.
80- `"initialization"`: Data loading during router initialization, such as initial loader requests in API-only Mode.
81- `"navigation"`: Data requests initiated by a navigation, including action submissions and the subsequent loader revalidation.
82- `"fetcher"`: Requests initiated by a fetcher, including the loader revalidation after a fetcher action.
83- `"revalidation"`: Requests initiated by an explicit revalidation, such as `useRevalidator().revalidate()`.
84
85`fetcherKey` identifies the fetcher targeted by a data request, or is `null` for route loader/action requests. A fetcher reloaded as part of a navigation or explicit revalidation retains that initiating `type` and also includes its fetcher key. Fetchers reloaded after an initialization redirect likewise retain the `"initialization"` type and include their fetcher key.
86
87`navigationType` is `"PUSH"`, `"REPLACE"`, or `"POP"` for navigation-initiated data requests, including fetchers reloaded by that navigation. It is `null` for initialization, standalone fetcher operations, and explicit revalidation. This is the history action for the in-flight navigation, not the previously committed location.
88
89## Examples
90
91Each example below can be passed to `HydratedRouter` as its `unstable_fetch` prop.
92
93### Authentication and Custom Headers
94
95Read the current token at request time so token changes don't require replacing the callback:
96
97```ts
98import type { unstable_RouterFetch } from "react-router/dom";
99import { authStore } from "./auth";
100
101const customFetch: unstable_RouterFetch = (request) => {
102 let headers = new Headers(request.headers);
103 let token = authStore.getAccessToken();
104
105 if (token) {
106 headers.set("Authorization", `Bearer ${token}`);
107 }
108
109 headers.set("X-App-Version", "1.0");
110 return fetch(request, { headers });
111};
112```
113
114Here, `authStore` is your application's authentication store. Only send credentials to trusted origins. Adding custom headers to cross-origin requests may require a CORS preflight and corresponding server configuration.
115
116### Cross-Origin Credentials
117
118In [API-only Mode](../start/framework/rendering#using-a-separate-api-origin), use `unstable_apiServerOrigin` to configure a separate API origin. React Router applies that origin before calling your custom fetch, and also uses it for native data prefetches.
119
120The origin setting does not enable cross-origin cookies. If you need them, customize the credential policy for the trusted API origin:
121
122```ts
123import type { unstable_RouterFetch } from "react-router/dom";
124
125const customFetch: unstable_RouterFetch = (request) => {
126 let isApiRequest =
127 new URL(request.url).origin ===
128 "https://api.example.com";
129
130 return fetch(request, {
131 credentials: isApiRequest
132 ? "include"
133 : request.credentials,
134 });
135};
136```
137
138The API server must allow credentialed CORS requests from your client origin. Browser cookie policies still apply. Native data prefetches do not use this callback, so don't rely on it to configure their credentials.
139
140### Back/Forward Traversal Cache
141
142Use `navigationType` to prefer the browser's HTTP cache when navigating back or forward, while retaining the normal cache policy for other requests:
143
144```ts
145import type { unstable_RouterFetch } from "react-router/dom";
146
147const customFetch: unstable_RouterFetch = (
148 request,
149 context,
150) => {
151 let isTraversal =
152 import.meta.env.PROD &&
153 request.method === "GET" &&
154 context.type === "navigation" &&
155 context.navigationType === "POP";
156
157 return fetch(request, {
158 cache: isTraversal ? "force-cache" : "default",
159 });
160};
161```
162
163This example enables the policy in production only. It includes GET requests for fetchers reloaded by a back/forward navigation, but not standalone fetcher loads or explicit revalidations.
164
165The [`force-cache` policy](https://fetch.spec.whatwg.org/#concept-request-cache-mode) uses a matching HTTP cache entry even if it is stale, falling back to the network when no entry exists. Your server's responses must be cacheable; `Cache-Control: no-store` responses aren't stored.
166
167This is not a router-managed data cache. React Router still performs its request and response decoding, and this policy adds no mutation or authentication invalidation guarantees. Only use it when serving potentially stale data is appropriate for your application.
168
169### Retrying Safe Requests
170
171You can retry a safe GET after a network failure without retrying action submissions or canceled requests:
172
173```ts
174import type { unstable_RouterFetch } from "react-router/dom";
175
176const customFetch: unstable_RouterFetch = async (
177 request,
178) => {
179 if (request.method !== "GET") {
180 return fetch(request);
181 }
182
183 try {
184 return await fetch(request.clone());
185 } catch (error) {
186 if (
187 request.signal.aborted ||
188 (error instanceof Error &&
189 error.name === "AbortError")
190 ) {
191 throw error;
192 }
193
194 // Retry once, retaining the original abort signal.
195 return fetch(request);
196 }
197};
198```
199
200This assumes your GET endpoints are safe to repeat. It retries a rejected fetch, not HTTP error responses such as `500`. Don't automatically retry mutations: the server may have processed an action even when the client didn't receive its response.
201
202## Limitations
203
204### Requests Outside Custom Fetch
205
206Custom fetch does not intercept the initial document request, server-side loader execution, static assets, or fetches you write in your own application code, including in `clientLoader`/`clientAction`. Calls to their `serverLoader`/`serverAction` helpers do use custom fetch when they issue server data requests.
207
208Data prefetches from `<Link prefetch>`, `<NavLink prefetch>`, and [`PrefetchPageLinks`](../api/components/PrefetchPageLinks) use native `<link rel="prefetch">` elements and bypass custom fetch. If a request requires custom headers or other transformations, use `prefetch="none"` on links and avoid rendering `PrefetchPageLinks` for those routes.
209
210### Inspecting Request Bodies
211
212If you need a form field such as `intent` to choose request headers, read it from `await request.clone().formData()` for form-encoded submissions, then send the original request. Reading `request.formData()` directly consumes the body so it can't subsequently be sent unchanged.
213
214Only parse bodies with a compatible encoding. Cloning and reading a body can buffer data, so avoid doing this unnecessarily, especially for large file uploads. Adding headers or changing fetch options does not require reading the body.
215
216### Response Compatibility
217
218Return the response from `fetch` unchanged. You can inspect its status and headers, but you should not consume its body or try to decode, modify, or re-encode it. React Router handles response decoding and expects its manifest/data protocol, including Turbo Stream-encoded loader/action data.