Skip to content

Commit 80b6ffa

Browse files
authored
Replace state on repeated Link navigations (#7864)
* Replace state on repeated Link navigations * Normalise possible string location to object * Add test for duplicate navigation detection * Spy on memoryRouter instead of mocking
1 parent df9d7d4 commit 80b6ffa

File tree

2 files changed

+56
-22
lines changed

2 files changed

+56
-22
lines changed

packages/react-router-dom/modules/Link.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from "react";
22
import { __RouterContext as RouterContext } from "react-router";
3+
import { createPath } from 'history';
34
import PropTypes from "prop-types";
45
import invariant from "tiny-invariant";
56
import {
@@ -100,7 +101,8 @@ const Link = forwardRef(
100101
href,
101102
navigate() {
102103
const location = resolveToLocation(to, context.location);
103-
const method = replace ? history.replace : history.push;
104+
const isDuplicateNavigation = createPath(context.location) === createPath(normalizeToLocation(location));
105+
const method = (replace || isDuplicateNavigation) ? history.replace : history.push;
104106

105107
method(location);
106108
}

packages/react-router-dom/modules/__tests__/Link-click-test.js

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,19 @@ import renderStrict from "./utils/renderStrict.js";
99
describe("<Link> click events", () => {
1010
const node = document.createElement("div");
1111

12-
afterEach(() => {
13-
ReactDOM.unmountComponentAtNode(node);
12+
let memoryHistory, pushSpy, replaceSpy;
13+
14+
beforeEach(() => {
15+
memoryHistory = createMemoryHistory();
16+
pushSpy = jest.spyOn(memoryHistory, "push");
17+
replaceSpy = jest.spyOn(memoryHistory, "push");
1418
});
1519

16-
const memoryHistory = createMemoryHistory();
17-
memoryHistory.push = jest.fn();
20+
afterEach(() => {
21+
ReactDOM.unmountComponentAtNode(node);
1822

19-
beforeEach(() => {
20-
memoryHistory.push.mockReset();
23+
pushSpy.mockRestore();
24+
replaceSpy.mockRestore();
2125
});
2226

2327
it("calls onClick eventhandler and history.push", () => {
@@ -40,15 +44,45 @@ describe("<Link> click events", () => {
4044
});
4145

4246
expect(clickHandler).toBeCalledTimes(1);
43-
expect(memoryHistory.push).toBeCalledTimes(1);
44-
expect(memoryHistory.push).toBeCalledWith(to);
47+
expect(pushSpy).toBeCalledTimes(1);
48+
expect(pushSpy).toBeCalledWith(to);
4549
});
4650

47-
it("calls onClick eventhandler and history.push with function `to` prop", () => {
48-
const memoryHistoryFoo = createMemoryHistory({
49-
initialEntries: ["/foo"]
51+
it("calls history.replace on duplicate navigation", () => {
52+
const clickHandler = jest.fn();
53+
const to = "/duplicate/path?the=query#the-hash";
54+
55+
renderStrict(
56+
<Router history={memoryHistory}>
57+
<Link to={to} onClick={clickHandler}>
58+
link
59+
</Link>
60+
</Router>,
61+
node
62+
);
63+
64+
const a = node.querySelector("a");
65+
TestUtils.Simulate.click(a, {
66+
defaultPrevented: false,
67+
button: 0
5068
});
51-
memoryHistoryFoo.push = jest.fn();
69+
70+
TestUtils.Simulate.click(a, {
71+
defaultPrevented: false,
72+
button: 0
73+
});
74+
75+
expect(clickHandler).toBeCalledTimes(2);
76+
expect(pushSpy).toBeCalledTimes(1);
77+
expect(pushSpy).toBeCalledWith(to);
78+
expect(replaceSpy).toBeCalledTimes(1);
79+
expect(replaceSpy).toBeCalledWith(to);
80+
});
81+
82+
it("calls onClick eventhandler and history.push with function `to` prop", () => {
83+
// Make push a no-op so key IDs do not change
84+
pushSpy.mockImplementation();
85+
5286
const clickHandler = jest.fn();
5387
let to = null;
5488
const toFn = location => {
@@ -61,7 +95,7 @@ describe("<Link> click events", () => {
6195
};
6296

6397
renderStrict(
64-
<Router history={memoryHistoryFoo}>
98+
<Router history={memoryHistory}>
6599
<Link to={toFn} onClick={clickHandler}>
66100
link
67101
</Link>
@@ -76,8 +110,8 @@ describe("<Link> click events", () => {
76110
});
77111

78112
expect(clickHandler).toBeCalledTimes(1);
79-
expect(memoryHistoryFoo.push).toBeCalledTimes(1);
80-
expect(memoryHistoryFoo.push).toBeCalledWith(to);
113+
expect(pushSpy).toBeCalledTimes(1);
114+
expect(pushSpy).toBeCalledWith(to);
81115
});
82116

83117
it("does not call history.push on right click", () => {
@@ -96,7 +130,7 @@ describe("<Link> click events", () => {
96130
button: 1
97131
});
98132

99-
expect(memoryHistory.push).toBeCalledTimes(0);
133+
expect(pushSpy).toBeCalledTimes(0);
100134
});
101135

102136
it("does not call history.push on prevented event.", () => {
@@ -115,7 +149,7 @@ describe("<Link> click events", () => {
115149
button: 0
116150
});
117151

118-
expect(memoryHistory.push).toBeCalledTimes(0);
152+
expect(pushSpy).toBeCalledTimes(0);
119153
});
120154

121155
it("does not call history.push target not specifying 'self'", () => {
@@ -136,12 +170,10 @@ describe("<Link> click events", () => {
136170
button: 0
137171
});
138172

139-
expect(memoryHistory.push).toBeCalledTimes(0);
173+
expect(pushSpy).toBeCalledTimes(0);
140174
});
141175

142176
it("prevents the default event handler if an error occurs", () => {
143-
const memoryHistory = createMemoryHistory();
144-
memoryHistory.push = jest.fn();
145177
const error = new Error();
146178
const clickHandler = () => {
147179
throw error;
@@ -173,6 +205,6 @@ describe("<Link> click events", () => {
173205
console.error.mockRestore();
174206
expect(clickHandler).toThrow(error);
175207
expect(mockPreventDefault).toHaveBeenCalled();
176-
expect(memoryHistory.push).toBeCalledTimes(0);
208+
expect(pushSpy).toBeCalledTimes(0);
177209
});
178210
});

0 commit comments

Comments
 (0)