diff --git a/packages/gatsby-link/src/__tests__/__snapshots__/index.js.snap b/packages/gatsby-link/src/__tests__/__snapshots__/index.js.snap
index 8ac8c603da7da..12e6254aad4dc 100644
--- a/packages/gatsby-link/src/__tests__/__snapshots__/index.js.snap
+++ b/packages/gatsby-link/src/__tests__/__snapshots__/index.js.snap
@@ -17,7 +17,7 @@ exports[` matches basic snapshot 1`] = `
link
diff --git a/packages/gatsby-link/src/__tests__/index.js b/packages/gatsby-link/src/__tests__/index.js
index c2a4b62956937..c813b0bb403e5 100644
--- a/packages/gatsby-link/src/__tests__/index.js
+++ b/packages/gatsby-link/src/__tests__/index.js
@@ -44,7 +44,30 @@ const getWithAssetPrefix = (prefix = ``) => {
return withAssetPrefix
}
-const setup = ({ sourcePath = `/active`, linkProps, pathPrefix = `` } = {}) => {
+const setup = ({ sourcePath = `/`, linkProps, pathPrefix = `` } = {}) => {
+ let intersectionInstances = new WeakMap()
+ // mock intersectionObserver
+ global.IntersectionObserver = jest.fn(cb => {
+ let instance = {
+ observe: ref => {
+ intersectionInstances.set(ref, instance)
+ },
+ unobserve: ref => {
+ intersectionInstances.delete(ref)
+ },
+ disconnect: () => {},
+ trigger: ref => {
+ cb([
+ {
+ target: ref,
+ isIntersecting: true,
+ },
+ ])
+ },
+ }
+
+ return instance
+ })
global.__BASE_PATH__ = pathPrefix
const source = createMemorySource(sourcePath)
const history = createHistory(source)
@@ -66,22 +89,31 @@ const setup = ({ sourcePath = `/active`, linkProps, pathPrefix = `` } = {}) => {
return Object.assign({}, utils, {
link: utils.getByText(`link`),
+ triggerInViewport: ref => {
+ intersectionInstances.get(ref).trigger(ref)
+ },
})
}
describe(``, () => {
it(`matches basic snapshot`, () => {
- const { container } = setup()
+ const { container } = setup({
+ linkProps: { to: `/active` },
+ })
expect(container).toMatchSnapshot()
})
it(`matches active snapshot`, () => {
- const { container } = setup({ linkProps: { to: `/active` } })
+ const { container } = setup({
+ sourcePath: `/active`,
+ linkProps: { to: `/active` },
+ })
expect(container).toMatchSnapshot()
})
it(`matches partially active snapshot`, () => {
const { container } = setup({
+ sourcePath: `/active`,
linkProps: { to: `/active/nested`, partiallyActive: true },
})
expect(container).toMatchSnapshot()
@@ -150,19 +182,28 @@ describe(``, () => {
it(`handles relative link with "./"`, () => {
const location = `./courses?sort=name`
- const { link } = setup({ linkProps: { to: location } })
+ const { link } = setup({
+ sourcePath: `/active`,
+ linkProps: { to: location },
+ })
expect(link.getAttribute(`href`)).toEqual(`/active/courses?sort=name`)
})
it(`handles relative link with "../"`, () => {
const location = `../courses?sort=name`
- const { link } = setup({ linkProps: { to: location } })
+ const { link } = setup({
+ sourcePath: `/active`,
+ linkProps: { to: location },
+ })
expect(link.getAttribute(`href`)).toEqual(`/courses?sort=name`)
})
it(`handles bare relative link`, () => {
const location = `courses?sort=name`
- const { link } = setup({ linkProps: { to: location } })
+ const { link } = setup({
+ sourcePath: `/active`,
+ linkProps: { to: location },
+ })
expect(link.getAttribute(`href`)).toEqual(`/active/courses?sort=name`)
})
@@ -383,3 +424,40 @@ describe(`state`, () => {
)
})
})
+
+describe(`prefetch`, () => {
+ beforeEach(() => {
+ global.___loader = {
+ enqueue: jest.fn(),
+ }
+ })
+
+ it(`it prefetches when in viewport`, () => {
+ const to = `/active`
+
+ const { link, triggerInViewport } = setup({
+ linkProps: { to },
+ })
+
+ triggerInViewport(link)
+
+ expect(global.___loader.enqueue).toHaveBeenCalledWith(
+ `${global.__BASE_PATH__}${to}`
+ )
+ })
+
+ it(`it does not prefetch if link is current page`, () => {
+ const to = `/active`
+
+ const { link, triggerInViewport } = setup({
+ sourcePath: `/active`,
+ linkProps: { to },
+ })
+
+ triggerInViewport(link)
+
+ expect(global.___loader.enqueue).not.toHaveBeenCalledWith(
+ `${global.__BASE_PATH__}${to}`
+ )
+ })
+})
diff --git a/packages/gatsby-link/src/index.js b/packages/gatsby-link/src/index.js
index 76e8fbf3d89cf..7d5a4f74ffa6d 100644
--- a/packages/gatsby-link/src/index.js
+++ b/packages/gatsby-link/src/index.js
@@ -93,6 +93,14 @@ const createIntersectionObserver = (el, cb) => {
return { instance: io, el }
}
+function GatsbyLinkLocationWrapper(props) {
+ return (
+
+ {({ location }) => }
+
+ )
+}
+
class GatsbyLink extends React.Component {
constructor(props) {
super(props)
@@ -108,23 +116,35 @@ class GatsbyLink extends React.Component {
this.handleRef = this.handleRef.bind(this)
}
+ _prefetch() {
+ let currentPath = window.location.pathname
+
+ // reach router should have the correct state
+ if (this.props._location && this.props._location.pathname) {
+ currentPath = this.props._location.pathname
+ }
+
+ const rewrittenPath = rewriteLinkPath(this.props.to, currentPath)
+ const newPathName = parsePath(rewrittenPath).pathname
+
+ // Prefech is used to speed up next navigations. When you use it on the current navigation,
+ // there could be a race-condition where Chrome uses the stale data instead of waiting for the network to complete
+ if (currentPath !== newPathName) {
+ ___loader.enqueue(newPathName)
+ }
+ }
+
componentDidUpdate(prevProps, prevState) {
// Preserve non IO functionality if no support
if (this.props.to !== prevProps.to && !this.state.IOSupported) {
- ___loader.enqueue(
- parsePath(rewriteLinkPath(this.props.to, window.location.pathname))
- .pathname
- )
+ this._prefetch()
}
}
componentDidMount() {
// Preserve non IO functionality if no support
if (!this.state.IOSupported) {
- ___loader.enqueue(
- parsePath(rewriteLinkPath(this.props.to, window.location.pathname))
- .pathname
- )
+ this._prefetch()
}
}
@@ -148,10 +168,7 @@ class GatsbyLink extends React.Component {
if (this.state.IOSupported && ref) {
// If IO supported and element reference found, setup Observer functionality
this.io = createIntersectionObserver(ref, () => {
- ___loader.enqueue(
- parsePath(rewriteLinkPath(this.props.to, window.location.pathname))
- .pathname
- )
+ this._prefetch()
})
}
}
@@ -181,70 +198,68 @@ class GatsbyLink extends React.Component {
partiallyActive,
state,
replace,
+ _location,
/* eslint-enable no-unused-vars */
...rest
} = this.props
+
if (process.env.NODE_ENV !== `production` && !isLocalLink(to)) {
console.warn(
`External link ${to} was detected in a Link component. Use the Link component only for internal links. See: https://gatsby.dev/internal-links`
)
}
+ const prefixedTo = rewriteLinkPath(to, _location.pathname)
+ if (!isLocalLink(prefixedTo)) {
+ return
+ }
+
return (
-
- {({ location }) => {
- const prefixedTo = rewriteLinkPath(to, location.pathname)
- return isLocalLink(prefixedTo) ? (
- {
- if (onMouseEnter) {
- onMouseEnter(e)
- }
- ___loader.hovering(parsePath(prefixedTo).pathname)
- }}
- onClick={e => {
- if (onClick) {
- onClick(e)
- }
-
- if (
- e.button === 0 && // ignore right clicks
- !this.props.target && // let browser handle "target=_blank"
- !e.defaultPrevented && // onClick prevented default
- !e.metaKey && // ignore clicks with modifier keys...
- !e.altKey &&
- !e.ctrlKey &&
- !e.shiftKey
- ) {
- e.preventDefault()
-
- let shouldReplace = replace
- const isCurrent =
- encodeURI(prefixedTo) === window.location.pathname
- if (typeof replace !== `boolean` && isCurrent) {
- shouldReplace = true
- }
- // Make sure the necessary scripts and data are
- // loaded before continuing.
- window.___navigate(prefixedTo, {
- state,
- replace: shouldReplace,
- })
- }
-
- return true
- }}
- {...rest}
- />
- ) : (
-
- )
+ {
+ if (onMouseEnter) {
+ onMouseEnter(e)
+ }
+ ___loader.hovering(parsePath(prefixedTo).pathname)
+ }}
+ onClick={e => {
+ if (onClick) {
+ onClick(e)
+ }
+
+ if (
+ e.button === 0 && // ignore right clicks
+ !this.props.target && // let browser handle "target=_blank"
+ !e.defaultPrevented && // onClick prevented default
+ !e.metaKey && // ignore clicks with modifier keys...
+ !e.altKey &&
+ !e.ctrlKey &&
+ !e.shiftKey
+ ) {
+ e.preventDefault()
+
+ let shouldReplace = replace
+ const isCurrent = encodeURI(prefixedTo) === _location.pathname
+
+ if (typeof replace !== `boolean` && isCurrent) {
+ shouldReplace = true
+ }
+ // Make sure the necessary scripts and data are
+ // loaded before continuing.
+ window.___navigate(prefixedTo, {
+ state,
+ replace: shouldReplace,
+ })
+ }
+
+ return true
}}
-
+ {...rest}
+ />
)
}
}
@@ -263,7 +278,7 @@ const showDeprecationWarning = (functionName, altFunctionName, version) =>
)
export default React.forwardRef((props, ref) => (
-
+
))
export const navigate = (to, options) => {