Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/x-remix-reload-document.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"react-router": minor
"react-router-dom": minor
"react-router-dom-v5-compat": minor
"react-router-native": minor
"@remix-run/router": minor
---

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
1 change: 1 addition & 0 deletions contributors.yml
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,4 @@
- smithki
- istarkov
- louis-young
- robbtraister
46 changes: 46 additions & 0 deletions docs/fetch/redirectDocument.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
title: redirectDocument
new: true
---

# `redirectDocument`

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.

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:

```jsx
import { redirectDocument } from "react-router-dom";

const loader = async () => {
const user = await getUser();
if (!user) {
return redirectDocument("/otherapp/login");
}
return null;
};
```

## Type Declaration

```ts
type RedirectFunction = (
url: string,
init?: number | ResponseInit
) => Response;
```

## `url`

The URL to redirect to.

```js
redirectDocument("/otherapp/login");
```

## `init`

The [Response][response] options to be used in the response.

[response]: https://developer.mozilla.org/en-US/docs/Web/API/Response/Response
[redirect]: ./redirect
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,19 @@
},
"filesize": {
"packages/router/dist/router.umd.min.js": {
"none": "47.2 kB"
"none": "47.5 kB"
},
"packages/react-router/dist/react-router.production.min.js": {
"none": "13.8 kB"
"none": "13.9 kB"
},
"packages/react-router/dist/umd/react-router.production.min.js": {
"none": "16.2 kB"
"none": "16.3 kB"
},
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
"none": "12.8 kB"
},
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
"none": "18.71 kB"
"none": "18.9 kB"
}
}
}
1 change: 1 addition & 0 deletions packages/react-router-dom-v5-compat/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ export {
Form,
json,
redirect,
redirectDocument,
useActionData,
useAsyncError,
useAsyncValue,
Expand Down
1 change: 1 addition & 0 deletions packages/react-router-dom/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export {
matchRoutes,
parsePath,
redirect,
redirectDocument,
renderMatches,
resolvePath,
useActionData,
Expand Down
1 change: 1 addition & 0 deletions packages/react-router-native/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export {
matchRoutes,
parsePath,
redirect,
redirectDocument,
renderMatches,
resolvePath,
useActionData,
Expand Down
2 changes: 2 additions & 0 deletions packages/react-router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
matchRoutes,
parsePath,
redirect,
redirectDocument,
resolvePath,
UNSAFE_warning as warning,
} from "@remix-run/router";
Expand Down Expand Up @@ -187,6 +188,7 @@ export {
matchRoutes,
parsePath,
redirect,
redirectDocument,
renderMatches,
resolvePath,
useActionData,
Expand Down
31 changes: 31 additions & 0 deletions packages/router/__tests__/router-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6979,6 +6979,37 @@ describe("a router", () => {
}
});

it("processes redirects with document reload if header is present (assign)", async () => {
let t = setup({ routes: REDIRECT_ROUTES });

let A = await t.navigate("/parent/child", {
formMethod: "post",
formData: createFormData({}),
});

await A.actions.child.redirectReturn("/redirect", 301, {
"X-Remix-Reload-Document": "true",
});
expect(t.window.location.assign).toHaveBeenCalledWith("/redirect");
expect(t.window.location.replace).not.toHaveBeenCalled();
});

it("processes redirects with document reload if header is present (replace)", async () => {
let t = setup({ routes: REDIRECT_ROUTES });

let A = await t.navigate("/parent/child", {
formMethod: "post",
formData: createFormData({}),
replace: true,
});

await A.actions.child.redirectReturn("/redirect", 301, {
"X-Remix-Reload-Document": "true",
});
expect(t.window.location.replace).toHaveBeenCalledWith("/redirect");
expect(t.window.location.assign).not.toHaveBeenCalled();
});

it("properly handles same-origin absolute URLs", async () => {
let t = setup({ routes: REDIRECT_ROUTES });

Expand Down
1 change: 1 addition & 0 deletions packages/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export {
matchRoutes,
normalizePathname,
redirect,
redirectDocument,
resolvePath,
resolveTo,
stripBasename,
Expand Down
22 changes: 17 additions & 5 deletions packages/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2111,12 +2111,23 @@ export function createRouter(init: RouterInit): Router {
redirectLocation,
"Expected a location on the redirect navigation"
);
// Check if this an absolute external redirect that goes to a new origin
if (ABSOLUTE_URL_REGEX.test(redirect.location) && isBrowser) {
let url = init.history.createURL(redirect.location);
let isDifferentBasename = stripBasename(url.pathname, basename) == null;

if (routerWindow.location.origin !== url.origin || isDifferentBasename) {
if (isBrowser) {
let isDocumentReload = false;

if (redirect.reloadDocument) {
// Hard reload if the response contained X-Remix-Reload-Document
isDocumentReload = true;
} else if (ABSOLUTE_URL_REGEX.test(redirect.location)) {
const url = init.history.createURL(redirect.location);
isDocumentReload =
// Hard reload if it's an absolute URL to a new origin
url.origin !== routerWindow.location.origin ||
// Hard reload if it's an absolute URL that does not match our basename
stripBasename(url.pathname, basename) == null;
}

if (isDocumentReload) {
if (replace) {
routerWindow.location.replace(redirect.location);
} else {
Expand Down Expand Up @@ -3734,6 +3745,7 @@ async function callLoaderOrAction(
status,
location,
revalidate: result.headers.get("X-Remix-Revalidate") !== null,
reloadDocument: result.headers.get("X-Remix-Reload-Document") !== null,
};
}

Expand Down
12 changes: 12 additions & 0 deletions packages/router/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export interface RedirectResult {
status: number;
location: string;
revalidate: boolean;
reloadDocument?: boolean;
}

/**
Expand Down Expand Up @@ -1484,6 +1485,17 @@ export const redirect: RedirectFunction = (url, init = 302) => {
});
};

/**
* A redirect response that will force a document reload to the new location.
* Sets the status code and the `Location` header.
* Defaults to "302 Found".
*/
export const redirectDocument: RedirectFunction = (url, init) => {
let response = redirect(url, init);
response.headers.set("X-Remix-Reload-Document", "true");
return response;
};

/**
* @private
* Utility class we use to hold auto-unwrapped 4xx/5xx Response bodies
Expand Down