Skip to content

Commit 46806a4

Browse files
Add support to force reload on redirect with X-Remix-Reload-Document header (#10705)
Co-authored-by: Matt Brophy <matt@brophy.org>
1 parent 2ab24cc commit 46806a4

File tree

12 files changed

+126
-9
lines changed

12 files changed

+126
-9
lines changed

.changeset/x-remix-reload-document.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"react-router": minor
3+
"react-router-dom": minor
4+
"react-router-dom-v5-compat": minor
5+
"react-router-native": minor
6+
"@remix-run/router": minor
7+
---
8+
9+
Add's a new `redirectDocument()` function which allows users to specify that a redirect from a `loader`/`action` should trigger a document reload (via `window.location`) instead of attempting to navigate to the redirected location via React Router

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,3 +228,4 @@
228228
- smithki
229229
- istarkov
230230
- louis-young
231+
- robbtraister

docs/fetch/redirectDocument.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
title: redirectDocument
3+
new: true
4+
---
5+
6+
# `redirectDocument`
7+
8+
This is a small wrapper around [`redirect`][redirect] that will trigger a document-level redirect to the new location instead of a client-side navigation.
9+
10+
This is most useful when you have a React Router app living next to a separate app on the same domain and need to redirect from the React Router app to the other app via `window.location` instead of a React Router navigation:
11+
12+
```jsx
13+
import { redirectDocument } from "react-router-dom";
14+
15+
const loader = async () => {
16+
const user = await getUser();
17+
if (!user) {
18+
return redirectDocument("/otherapp/login");
19+
}
20+
return null;
21+
};
22+
```
23+
24+
## Type Declaration
25+
26+
```ts
27+
type RedirectFunction = (
28+
url: string,
29+
init?: number | ResponseInit
30+
) => Response;
31+
```
32+
33+
## `url`
34+
35+
The URL to redirect to.
36+
37+
```js
38+
redirectDocument("/otherapp/login");
39+
```
40+
41+
## `init`
42+
43+
The [Response][response] options to be used in the response.
44+
45+
[response]: https://developer.mozilla.org/en-US/docs/Web/API/Response/Response
46+
[redirect]: ./redirect

package.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,19 +109,19 @@
109109
},
110110
"filesize": {
111111
"packages/router/dist/router.umd.min.js": {
112-
"none": "47.2 kB"
112+
"none": "47.5 kB"
113113
},
114114
"packages/react-router/dist/react-router.production.min.js": {
115-
"none": "13.8 kB"
115+
"none": "13.9 kB"
116116
},
117117
"packages/react-router/dist/umd/react-router.production.min.js": {
118-
"none": "16.2 kB"
118+
"none": "16.3 kB"
119119
},
120120
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
121121
"none": "12.8 kB"
122122
},
123123
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
124-
"none": "18.71 kB"
124+
"none": "18.9 kB"
125125
}
126126
}
127127
}

packages/react-router-dom-v5-compat/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ export {
158158
Form,
159159
json,
160160
redirect,
161+
redirectDocument,
161162
useActionData,
162163
useAsyncError,
163164
useAsyncValue,

packages/react-router-dom/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ export {
152152
matchRoutes,
153153
parsePath,
154154
redirect,
155+
redirectDocument,
155156
renderMatches,
156157
resolvePath,
157158
useActionData,

packages/react-router-native/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ export {
8787
matchRoutes,
8888
parsePath,
8989
redirect,
90+
redirectDocument,
9091
renderMatches,
9192
resolvePath,
9293
useActionData,

packages/react-router/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
matchRoutes,
4040
parsePath,
4141
redirect,
42+
redirectDocument,
4243
resolvePath,
4344
UNSAFE_warning as warning,
4445
} from "@remix-run/router";
@@ -187,6 +188,7 @@ export {
187188
matchRoutes,
188189
parsePath,
189190
redirect,
191+
redirectDocument,
190192
renderMatches,
191193
resolvePath,
192194
useActionData,

packages/router/__tests__/router-test.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6979,6 +6979,37 @@ describe("a router", () => {
69796979
}
69806980
});
69816981

6982+
it("processes redirects with document reload if header is present (assign)", async () => {
6983+
let t = setup({ routes: REDIRECT_ROUTES });
6984+
6985+
let A = await t.navigate("/parent/child", {
6986+
formMethod: "post",
6987+
formData: createFormData({}),
6988+
});
6989+
6990+
await A.actions.child.redirectReturn("/redirect", 301, {
6991+
"X-Remix-Reload-Document": "true",
6992+
});
6993+
expect(t.window.location.assign).toHaveBeenCalledWith("/redirect");
6994+
expect(t.window.location.replace).not.toHaveBeenCalled();
6995+
});
6996+
6997+
it("processes redirects with document reload if header is present (replace)", async () => {
6998+
let t = setup({ routes: REDIRECT_ROUTES });
6999+
7000+
let A = await t.navigate("/parent/child", {
7001+
formMethod: "post",
7002+
formData: createFormData({}),
7003+
replace: true,
7004+
});
7005+
7006+
await A.actions.child.redirectReturn("/redirect", 301, {
7007+
"X-Remix-Reload-Document": "true",
7008+
});
7009+
expect(t.window.location.replace).toHaveBeenCalledWith("/redirect");
7010+
expect(t.window.location.assign).not.toHaveBeenCalled();
7011+
});
7012+
69827013
it("properly handles same-origin absolute URLs", async () => {
69837014
let t = setup({ routes: REDIRECT_ROUTES });
69847015

packages/router/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export {
3939
matchRoutes,
4040
normalizePathname,
4141
redirect,
42+
redirectDocument,
4243
resolvePath,
4344
resolveTo,
4445
stripBasename,

packages/router/router.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2111,12 +2111,23 @@ export function createRouter(init: RouterInit): Router {
21112111
redirectLocation,
21122112
"Expected a location on the redirect navigation"
21132113
);
2114-
// Check if this an absolute external redirect that goes to a new origin
2115-
if (ABSOLUTE_URL_REGEX.test(redirect.location) && isBrowser) {
2116-
let url = init.history.createURL(redirect.location);
2117-
let isDifferentBasename = stripBasename(url.pathname, basename) == null;
21182114

2119-
if (routerWindow.location.origin !== url.origin || isDifferentBasename) {
2115+
if (isBrowser) {
2116+
let isDocumentReload = false;
2117+
2118+
if (redirect.reloadDocument) {
2119+
// Hard reload if the response contained X-Remix-Reload-Document
2120+
isDocumentReload = true;
2121+
} else if (ABSOLUTE_URL_REGEX.test(redirect.location)) {
2122+
const url = init.history.createURL(redirect.location);
2123+
isDocumentReload =
2124+
// Hard reload if it's an absolute URL to a new origin
2125+
url.origin !== routerWindow.location.origin ||
2126+
// Hard reload if it's an absolute URL that does not match our basename
2127+
stripBasename(url.pathname, basename) == null;
2128+
}
2129+
2130+
if (isDocumentReload) {
21202131
if (replace) {
21212132
routerWindow.location.replace(redirect.location);
21222133
} else {
@@ -3734,6 +3745,7 @@ async function callLoaderOrAction(
37343745
status,
37353746
location,
37363747
revalidate: result.headers.get("X-Remix-Revalidate") !== null,
3748+
reloadDocument: result.headers.get("X-Remix-Reload-Document") !== null,
37373749
};
37383750
}
37393751

packages/router/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface RedirectResult {
4343
status: number;
4444
location: string;
4545
revalidate: boolean;
46+
reloadDocument?: boolean;
4647
}
4748

4849
/**
@@ -1484,6 +1485,17 @@ export const redirect: RedirectFunction = (url, init = 302) => {
14841485
});
14851486
};
14861487

1488+
/**
1489+
* A redirect response that will force a document reload to the new location.
1490+
* Sets the status code and the `Location` header.
1491+
* Defaults to "302 Found".
1492+
*/
1493+
export const redirectDocument: RedirectFunction = (url, init) => {
1494+
let response = redirect(url, init);
1495+
response.headers.set("X-Remix-Reload-Document", "true");
1496+
return response;
1497+
};
1498+
14871499
/**
14881500
* @private
14891501
* Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies

0 commit comments

Comments
 (0)