Skip to content

Commit

Permalink
Add support to force reload on redirect with `X-Remix-Reload-Document…
Browse files Browse the repository at this point in the history
…` header (#10705)

Co-authored-by: Matt Brophy <matt@brophy.org>
  • Loading branch information
robbtraister and brophdawg11 committed Aug 2, 2023
1 parent 2ab24cc commit 46806a4
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 9 deletions.
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

0 comments on commit 46806a4

Please sign in to comment.