Skip to content

Commit 3666130

Browse files
wardpeetgatsbybot
and
gatsbybot
authored
fix(gatsby-link): don't prefetch same page (#28307)
* fix(gatsby-link): don't prefetch same page * test prefect intersection-observer Co-authored-by: gatsbybot <[email protected]>
1 parent 613f5c7 commit 3666130

File tree

3 files changed

+166
-73
lines changed

3 files changed

+166
-73
lines changed

packages/gatsby-link/src/__tests__/__snapshots__/index.js.snap

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ exports[`<Link /> matches basic snapshot 1`] = `
1717
<div>
1818
<a
1919
class="link"
20-
href="/"
20+
href="/active"
2121
style="color: black;"
2222
>
2323
link

packages/gatsby-link/src/__tests__/index.js

Lines changed: 84 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,30 @@ const getWithAssetPrefix = (prefix = ``) => {
4444
return withAssetPrefix
4545
}
4646

47-
const setup = ({ sourcePath = `/active`, linkProps, pathPrefix = `` } = {}) => {
47+
const setup = ({ sourcePath = `/`, linkProps, pathPrefix = `` } = {}) => {
48+
let intersectionInstances = new WeakMap()
49+
// mock intersectionObserver
50+
global.IntersectionObserver = jest.fn(cb => {
51+
let instance = {
52+
observe: ref => {
53+
intersectionInstances.set(ref, instance)
54+
},
55+
unobserve: ref => {
56+
intersectionInstances.delete(ref)
57+
},
58+
disconnect: () => {},
59+
trigger: ref => {
60+
cb([
61+
{
62+
target: ref,
63+
isIntersecting: true,
64+
},
65+
])
66+
},
67+
}
68+
69+
return instance
70+
})
4871
global.__BASE_PATH__ = pathPrefix
4972
const source = createMemorySource(sourcePath)
5073
const history = createHistory(source)
@@ -66,22 +89,31 @@ const setup = ({ sourcePath = `/active`, linkProps, pathPrefix = `` } = {}) => {
6689

6790
return Object.assign({}, utils, {
6891
link: utils.getByText(`link`),
92+
triggerInViewport: ref => {
93+
intersectionInstances.get(ref).trigger(ref)
94+
},
6995
})
7096
}
7197

7298
describe(`<Link />`, () => {
7399
it(`matches basic snapshot`, () => {
74-
const { container } = setup()
100+
const { container } = setup({
101+
linkProps: { to: `/active` },
102+
})
75103
expect(container).toMatchSnapshot()
76104
})
77105

78106
it(`matches active snapshot`, () => {
79-
const { container } = setup({ linkProps: { to: `/active` } })
107+
const { container } = setup({
108+
sourcePath: `/active`,
109+
linkProps: { to: `/active` },
110+
})
80111
expect(container).toMatchSnapshot()
81112
})
82113

83114
it(`matches partially active snapshot`, () => {
84115
const { container } = setup({
116+
sourcePath: `/active`,
85117
linkProps: { to: `/active/nested`, partiallyActive: true },
86118
})
87119
expect(container).toMatchSnapshot()
@@ -150,19 +182,28 @@ describe(`<Link />`, () => {
150182

151183
it(`handles relative link with "./"`, () => {
152184
const location = `./courses?sort=name`
153-
const { link } = setup({ linkProps: { to: location } })
185+
const { link } = setup({
186+
sourcePath: `/active`,
187+
linkProps: { to: location },
188+
})
154189
expect(link.getAttribute(`href`)).toEqual(`/active/courses?sort=name`)
155190
})
156191

157192
it(`handles relative link with "../"`, () => {
158193
const location = `../courses?sort=name`
159-
const { link } = setup({ linkProps: { to: location } })
194+
const { link } = setup({
195+
sourcePath: `/active`,
196+
linkProps: { to: location },
197+
})
160198
expect(link.getAttribute(`href`)).toEqual(`/courses?sort=name`)
161199
})
162200

163201
it(`handles bare relative link`, () => {
164202
const location = `courses?sort=name`
165-
const { link } = setup({ linkProps: { to: location } })
203+
const { link } = setup({
204+
sourcePath: `/active`,
205+
linkProps: { to: location },
206+
})
166207
expect(link.getAttribute(`href`)).toEqual(`/active/courses?sort=name`)
167208
})
168209

@@ -383,3 +424,40 @@ describe(`state`, () => {
383424
)
384425
})
385426
})
427+
428+
describe(`prefetch`, () => {
429+
beforeEach(() => {
430+
global.___loader = {
431+
enqueue: jest.fn(),
432+
}
433+
})
434+
435+
it(`it prefetches when in viewport`, () => {
436+
const to = `/active`
437+
438+
const { link, triggerInViewport } = setup({
439+
linkProps: { to },
440+
})
441+
442+
triggerInViewport(link)
443+
444+
expect(global.___loader.enqueue).toHaveBeenCalledWith(
445+
`${global.__BASE_PATH__}${to}`
446+
)
447+
})
448+
449+
it(`it does not prefetch if link is current page`, () => {
450+
const to = `/active`
451+
452+
const { link, triggerInViewport } = setup({
453+
sourcePath: `/active`,
454+
linkProps: { to },
455+
})
456+
457+
triggerInViewport(link)
458+
459+
expect(global.___loader.enqueue).not.toHaveBeenCalledWith(
460+
`${global.__BASE_PATH__}${to}`
461+
)
462+
})
463+
})

packages/gatsby-link/src/index.js

Lines changed: 81 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,14 @@ const createIntersectionObserver = (el, cb) => {
9393
return { instance: io, el }
9494
}
9595

96+
function GatsbyLinkLocationWrapper(props) {
97+
return (
98+
<Location>
99+
{({ location }) => <GatsbyLink {...props} _location={location} />}
100+
</Location>
101+
)
102+
}
103+
96104
class GatsbyLink extends React.Component {
97105
constructor(props) {
98106
super(props)
@@ -108,23 +116,35 @@ class GatsbyLink extends React.Component {
108116
this.handleRef = this.handleRef.bind(this)
109117
}
110118

119+
_prefetch() {
120+
let currentPath = window.location.pathname
121+
122+
// reach router should have the correct state
123+
if (this.props._location && this.props._location.pathname) {
124+
currentPath = this.props._location.pathname
125+
}
126+
127+
const rewrittenPath = rewriteLinkPath(this.props.to, currentPath)
128+
const newPathName = parsePath(rewrittenPath).pathname
129+
130+
// Prefech is used to speed up next navigations. When you use it on the current navigation,
131+
// there could be a race-condition where Chrome uses the stale data instead of waiting for the network to complete
132+
if (currentPath !== newPathName) {
133+
___loader.enqueue(newPathName)
134+
}
135+
}
136+
111137
componentDidUpdate(prevProps, prevState) {
112138
// Preserve non IO functionality if no support
113139
if (this.props.to !== prevProps.to && !this.state.IOSupported) {
114-
___loader.enqueue(
115-
parsePath(rewriteLinkPath(this.props.to, window.location.pathname))
116-
.pathname
117-
)
140+
this._prefetch()
118141
}
119142
}
120143

121144
componentDidMount() {
122145
// Preserve non IO functionality if no support
123146
if (!this.state.IOSupported) {
124-
___loader.enqueue(
125-
parsePath(rewriteLinkPath(this.props.to, window.location.pathname))
126-
.pathname
127-
)
147+
this._prefetch()
128148
}
129149
}
130150

@@ -148,10 +168,7 @@ class GatsbyLink extends React.Component {
148168
if (this.state.IOSupported && ref) {
149169
// If IO supported and element reference found, setup Observer functionality
150170
this.io = createIntersectionObserver(ref, () => {
151-
___loader.enqueue(
152-
parsePath(rewriteLinkPath(this.props.to, window.location.pathname))
153-
.pathname
154-
)
171+
this._prefetch()
155172
})
156173
}
157174
}
@@ -181,70 +198,68 @@ class GatsbyLink extends React.Component {
181198
partiallyActive,
182199
state,
183200
replace,
201+
_location,
184202
/* eslint-enable no-unused-vars */
185203
...rest
186204
} = this.props
205+
187206
if (process.env.NODE_ENV !== `production` && !isLocalLink(to)) {
188207
console.warn(
189208
`External link ${to} was detected in a Link component. Use the Link component only for internal links. See: https://gatsby.dev/internal-links`
190209
)
191210
}
192211

212+
const prefixedTo = rewriteLinkPath(to, _location.pathname)
213+
if (!isLocalLink(prefixedTo)) {
214+
return <a href={prefixedTo} {...rest} />
215+
}
216+
193217
return (
194-
<Location>
195-
{({ location }) => {
196-
const prefixedTo = rewriteLinkPath(to, location.pathname)
197-
return isLocalLink(prefixedTo) ? (
198-
<Link
199-
to={prefixedTo}
200-
state={state}
201-
getProps={getProps}
202-
innerRef={this.handleRef}
203-
onMouseEnter={e => {
204-
if (onMouseEnter) {
205-
onMouseEnter(e)
206-
}
207-
___loader.hovering(parsePath(prefixedTo).pathname)
208-
}}
209-
onClick={e => {
210-
if (onClick) {
211-
onClick(e)
212-
}
213-
214-
if (
215-
e.button === 0 && // ignore right clicks
216-
!this.props.target && // let browser handle "target=_blank"
217-
!e.defaultPrevented && // onClick prevented default
218-
!e.metaKey && // ignore clicks with modifier keys...
219-
!e.altKey &&
220-
!e.ctrlKey &&
221-
!e.shiftKey
222-
) {
223-
e.preventDefault()
224-
225-
let shouldReplace = replace
226-
const isCurrent =
227-
encodeURI(prefixedTo) === window.location.pathname
228-
if (typeof replace !== `boolean` && isCurrent) {
229-
shouldReplace = true
230-
}
231-
// Make sure the necessary scripts and data are
232-
// loaded before continuing.
233-
window.___navigate(prefixedTo, {
234-
state,
235-
replace: shouldReplace,
236-
})
237-
}
238-
239-
return true
240-
}}
241-
{...rest}
242-
/>
243-
) : (
244-
<a href={prefixedTo} {...rest} />
245-
)
218+
<Link
219+
to={prefixedTo}
220+
state={state}
221+
getProps={getProps}
222+
innerRef={this.handleRef}
223+
onMouseEnter={e => {
224+
if (onMouseEnter) {
225+
onMouseEnter(e)
226+
}
227+
___loader.hovering(parsePath(prefixedTo).pathname)
228+
}}
229+
onClick={e => {
230+
if (onClick) {
231+
onClick(e)
232+
}
233+
234+
if (
235+
e.button === 0 && // ignore right clicks
236+
!this.props.target && // let browser handle "target=_blank"
237+
!e.defaultPrevented && // onClick prevented default
238+
!e.metaKey && // ignore clicks with modifier keys...
239+
!e.altKey &&
240+
!e.ctrlKey &&
241+
!e.shiftKey
242+
) {
243+
e.preventDefault()
244+
245+
let shouldReplace = replace
246+
const isCurrent = encodeURI(prefixedTo) === _location.pathname
247+
248+
if (typeof replace !== `boolean` && isCurrent) {
249+
shouldReplace = true
250+
}
251+
// Make sure the necessary scripts and data are
252+
// loaded before continuing.
253+
window.___navigate(prefixedTo, {
254+
state,
255+
replace: shouldReplace,
256+
})
257+
}
258+
259+
return true
246260
}}
247-
</Location>
261+
{...rest}
262+
/>
248263
)
249264
}
250265
}
@@ -263,7 +278,7 @@ const showDeprecationWarning = (functionName, altFunctionName, version) =>
263278
)
264279

265280
export default React.forwardRef((props, ref) => (
266-
<GatsbyLink innerRef={ref} {...props} />
281+
<GatsbyLinkLocationWrapper innerRef={ref} {...props} />
267282
))
268283

269284
export const navigate = (to, options) => {

0 commit comments

Comments
 (0)