Skip to content

Commit 01d0f41

Browse files
Add support for replace() redirects (#11811)
Co-authored-by: Brendan Allan <[email protected]>
1 parent bc5b15d commit 01d0f41

File tree

11 files changed

+103
-5
lines changed

11 files changed

+103
-5
lines changed

.changeset/five-bottles-press.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@remix-run/router": minor
3+
"react-router": minor
4+
"react-router-dom": minor
5+
---
6+
7+
Add a new `replace(url, init?)` alternative to `redirect(url, init?)` that performs a `history.replaceState` instead of a `history.pushState` on client-side navigation redirects

contributors.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
- bhbs
3939
- bilalk711
4040
- bobziroll
41+
- Brendonovich
4142
- BrianT1414
4243
- brockross
4344
- brookslybrand

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,13 +111,13 @@
111111
"none": "14.9 kB"
112112
},
113113
"packages/react-router/dist/umd/react-router.production.min.js": {
114-
"none": "17.3 kB"
114+
"none": "17.4 kB"
115115
},
116116
"packages/react-router-dom/dist/react-router-dom.production.min.js": {
117117
"none": "17.3 kB"
118118
},
119119
"packages/react-router-dom/dist/umd/react-router-dom.production.min.js": {
120-
"none": "23.6 kB"
120+
"none": "23.7 kB"
121121
}
122122
},
123123
"pnpm": {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ export {
154154
parsePath,
155155
redirect,
156156
redirectDocument,
157+
replace,
157158
renderMatches,
158159
resolvePath,
159160
unstable_HistoryRouter,

packages/react-router-dom/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ export {
178178
parsePath,
179179
redirect,
180180
redirectDocument,
181+
replace,
181182
renderMatches,
182183
resolvePath,
183184
useActionData,

packages/react-router-native/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ export {
9797
parsePath,
9898
redirect,
9999
redirectDocument,
100+
replace,
100101
renderMatches,
101102
resolvePath,
102103
useActionData,

packages/react-router/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
parsePath,
5050
redirect,
5151
redirectDocument,
52+
replace,
5253
resolvePath,
5354
UNSAFE_warning as warning,
5455
} from "@remix-run/router";
@@ -206,6 +207,7 @@ export {
206207
parsePath,
207208
redirect,
208209
redirectDocument,
210+
replace,
209211
renderMatches,
210212
resolvePath,
211213
useBlocker,

packages/router/__tests__/redirects-test.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
1-
import { IDLE_NAVIGATION } from "../index";
1+
import {
2+
IDLE_NAVIGATION,
3+
createBrowserHistory,
4+
createMemoryHistory,
5+
createRouter,
6+
} from "../index";
7+
import { replace } from "../utils";
28
import type { TestRouteObject } from "./utils/data-router-setup";
39
import { cleanup, setup } from "./utils/data-router-setup";
4-
import { createFormData } from "./utils/utils";
10+
import { createFormData, tick } from "./utils/utils";
511

612
describe("redirects", () => {
713
afterEach(() => cleanup());
@@ -642,6 +648,70 @@ describe("redirects", () => {
642648
});
643649
});
644650

651+
it("supports replace() redirects", async () => {
652+
let router = createRouter({
653+
history: createMemoryHistory(),
654+
routes: [
655+
{
656+
path: "/",
657+
},
658+
{
659+
path: "/a",
660+
},
661+
{
662+
path: "/b",
663+
loader: () => replace("/c"),
664+
},
665+
{
666+
path: "/c",
667+
},
668+
],
669+
});
670+
router.initialize();
671+
await tick();
672+
673+
// ['/']
674+
expect(router.state).toMatchObject({
675+
historyAction: "POP",
676+
location: {
677+
pathname: "/",
678+
state: null,
679+
},
680+
});
681+
682+
// Push /a: ['/', '/a']
683+
await router.navigate("/a");
684+
expect(router.state).toMatchObject({
685+
historyAction: "PUSH",
686+
location: {
687+
pathname: "/a",
688+
state: null,
689+
},
690+
});
691+
692+
// Push /b which calls replace('/c'): ['/', '/c']
693+
await router.navigate("/b");
694+
expect(router.state).toMatchObject({
695+
historyAction: "REPLACE",
696+
location: {
697+
pathname: "/c",
698+
state: {
699+
_isRedirect: true,
700+
},
701+
},
702+
});
703+
704+
// Pop: ['/']
705+
await router.navigate(-1);
706+
expect(router.state).toMatchObject({
707+
historyAction: "POP",
708+
location: {
709+
pathname: "/",
710+
state: null,
711+
},
712+
});
713+
});
714+
645715
describe("redirect status code handling", () => {
646716
it("should not treat 300 as a redirect", async () => {
647717
let t = setup({ routes: REDIRECT_ROUTES });

packages/router/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export {
4848
normalizePathname,
4949
redirect,
5050
redirectDocument,
51+
replace,
5152
resolvePath,
5253
resolveTo,
5354
stripBasename,

packages/router/router.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2683,7 +2683,9 @@ export function createRouter(init: RouterInit): Router {
26832683
pendingNavigationController = null;
26842684

26852685
let redirectHistoryAction =
2686-
replace === true ? HistoryAction.Replace : HistoryAction.Push;
2686+
replace === true || redirect.response.headers.has("X-Remix-Replace")
2687+
? HistoryAction.Replace
2688+
: HistoryAction.Push;
26872689

26882690
// Use the incoming submission if provided, fallback on the active one in
26892691
// state.navigation

packages/router/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1619,6 +1619,18 @@ export const redirectDocument: RedirectFunction = (url, init) => {
16191619
return response;
16201620
};
16211621

1622+
/**
1623+
* A redirect response that will perform a `history.replaceState` instead of a
1624+
* `history.pushState` for client-side navigation redirects.
1625+
* Sets the status code and the `Location` header.
1626+
* Defaults to "302 Found".
1627+
*/
1628+
export const replace: RedirectFunction = (url, init) => {
1629+
let response = redirect(url, init);
1630+
response.headers.set("X-Remix-Replace", "true");
1631+
return response;
1632+
};
1633+
16221634
export type ErrorResponse = {
16231635
status: number;
16241636
statusText: string;

0 commit comments

Comments
 (0)