Skip to content

Commit 844ad54

Browse files
authored
Merge pull request #7590 from maiieul/1.14.0-alpha-fix-SPA-nav-2
fix(qwik-city): SPA link prefetching on subsequent navigations
2 parents d54c51d + b6774bb commit 844ad54

File tree

4 files changed

+50
-79
lines changed

4 files changed

+50
-79
lines changed

.changeset/thick-trams-pay.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@builder.io/qwik-city': patch
3+
---
4+
5+
🩹 Link SPA subsequent navigation now properly prefetch the next routes.

packages/qwik-city/src/runtime/src/link-component.tsx

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { component$, Slot, type QwikIntrinsicElements, untrack, $, sync$ } from '@builder.io/qwik';
2-
import { getClientNavPath, shouldPrefetchData, shouldPrefetchSymbols } from './utils';
1+
import {
2+
component$,
3+
Slot,
4+
type QwikIntrinsicElements,
5+
$,
6+
sync$,
7+
useSignal,
8+
useVisibleTask$,
9+
type QwikVisibleEvent,
10+
untrack,
11+
} from '@builder.io/qwik';
12+
import { getClientNavPath, shouldPreload } from './utils';
313
import { loadClientData } from './use-endpoint';
414
import { useLocation, useNavigate } from './use-functions';
515
import { prefetchSymbols } from './client-navigate';
@@ -10,6 +20,7 @@ export const Link = component$<LinkProps>((props) => {
1020
const nav = useNavigate();
1121
const loc = useLocation();
1222
const originalHref = props.href;
23+
const anchorRef = useSignal<HTMLAnchorElement>();
1324
const {
1425
onClick$,
1526
prefetch: prefetchProp,
@@ -22,18 +33,13 @@ export const Link = component$<LinkProps>((props) => {
2233
linkProps.href = clientNavPath || originalHref;
2334

2435
const prefetchData = untrack(
25-
() =>
26-
(!!clientNavPath &&
27-
prefetchProp !== false &&
28-
prefetchProp !== 'js' &&
29-
shouldPrefetchData(clientNavPath, loc)) ||
30-
undefined
36+
() => (!!clientNavPath && prefetchProp !== false && prefetchProp !== 'js') || undefined
3137
);
3238

3339
const prefetch = untrack(
3440
() =>
3541
prefetchData ||
36-
(!!clientNavPath && prefetchProp !== false && shouldPrefetchSymbols(clientNavPath, loc))
42+
(!!clientNavPath && prefetchProp !== false && shouldPreload(clientNavPath, loc))
3743
);
3844

3945
const handlePrefetch = prefetch
@@ -77,17 +83,36 @@ export const Link = component$<LinkProps>((props) => {
7783
}
7884
})
7985
: undefined;
86+
87+
useVisibleTask$(({ track }) => {
88+
track(() => loc.url.pathname);
89+
if (linkProps.onQVisible$) {
90+
const event = new CustomEvent('qvisible') as QwikVisibleEvent;
91+
92+
if (Array.isArray(linkProps.onQVisible$)) {
93+
linkProps.onQVisible$
94+
.flat(10)
95+
.forEach((handler) => (handler as any)?.(event, anchorRef.value));
96+
} else {
97+
linkProps.onQVisible$?.(event, anchorRef.value!);
98+
}
99+
}
100+
// Don't prefetch on visible in dev mode
101+
if (!isDev && anchorRef.value) {
102+
handlePrefetch?.(undefined, anchorRef.value!);
103+
}
104+
});
105+
80106
return (
81107
<a
108+
ref={anchorRef}
82109
// Attr 'q:link' is used as a selector for bootstrapping into spa after context loss
83110
{...{ 'q:link': !!clientNavPath }}
84111
{...linkProps}
85112
onClick$={[preventDefault, onClick$, handleClick]}
86113
data-prefetch={prefetchData}
87114
onMouseOver$={[linkProps.onMouseOver$, handlePrefetch]}
88115
onFocus$={[linkProps.onFocus$, handlePrefetch]}
89-
// Don't prefetch on visible in dev mode
90-
onQVisible$={[linkProps.onQVisible$, !isDev ? handlePrefetch : undefined]}
91116
>
92117
<Slot />
93118
</a>

packages/qwik-city/src/runtime/src/utils.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,16 +58,7 @@ export const getClientNavPath = (props: Record<string, any>, baseUrl: { url: URL
5858
return null;
5959
};
6060

61-
export const shouldPrefetchData = (clientNavPath: string | null, currentLoc: { url: URL }) => {
62-
if (clientNavPath) {
63-
const prefetchUrl = toUrl(clientNavPath, currentLoc.url);
64-
const currentUrl = toUrl('', currentLoc.url);
65-
return !isSamePath(prefetchUrl, currentUrl);
66-
}
67-
return false;
68-
};
69-
70-
export const shouldPrefetchSymbols = (clientNavPath: string | null, currentLoc: { url: URL }) => {
61+
export const shouldPreload = (clientNavPath: string | null, currentLoc: { url: URL }) => {
7162
if (clientNavPath) {
7263
const prefetchUrl = toUrl(clientNavPath, currentLoc.url);
7364
const currentUrl = toUrl('', currentLoc.url);

packages/qwik-city/src/runtime/src/utils.unit.ts

Lines changed: 8 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { assert, test } from 'vitest';
22
import {
33
getClientDataPath,
44
getClientNavPath,
5-
shouldPrefetchData,
6-
shouldPrefetchSymbols,
5+
shouldPreload,
76
isSameOrigin,
87
isSameOriginDifferentPathname,
98
isSamePath,
@@ -189,94 +188,45 @@ test(`isSameOrigin`, () => {
189188
});
190189
});
191190

192-
test('missing clientNavPath', () => {
193-
const clientNavPath = null;
194-
const currentLoc = new URL('https://qwik.dev/contact');
195-
assert.equal(shouldPrefetchData(clientNavPath, { url: currentLoc }), false);
196-
});
197-
198-
test('path and current path the same, has different querystring and hash', () => {
199-
const clientNavPath = '/about?qs#hash';
200-
const currentLoc = new URL('https://qwik.dev/about');
201-
assert.equal(shouldPrefetchData(clientNavPath, { url: currentLoc }), true);
202-
});
203-
204-
test('path and current path the same, querystring the same', () => {
205-
const clientNavPath = '/about?qs';
206-
const currentLoc = new URL('https://qwik.dev/about?qs');
207-
assert.equal(shouldPrefetchData(clientNavPath, { url: currentLoc }), false);
208-
});
209-
210-
test('path and current path the same', () => {
211-
const clientNavPath = '/about';
212-
const currentLoc = new URL('https://qwik.dev/about');
213-
assert.equal(shouldPrefetchData(clientNavPath, { url: currentLoc }), false);
214-
});
215-
216-
test('path and current path the same, different trailing slash', () => {
217-
const clientNavPath = '/about/';
218-
const currentLoc = new URL('https://qwik.dev/about');
219-
assert.equal(shouldPrefetchData(clientNavPath, { url: currentLoc }), false);
220-
});
221-
222-
test('valid prefetchUrl, has querystring and hash', () => {
223-
const clientNavPath = '/about?qs#hash';
224-
const currentLoc = new URL('https://qwik.dev/contact');
225-
assert.equal(shouldPrefetchData(clientNavPath, { url: currentLoc }), true);
226-
});
227-
228-
test('valid prefetchUrl, trailing slash', () => {
229-
const clientNavPath = '/about/';
230-
const currentLoc = new URL('https://qwik.dev/contact');
231-
assert.equal(shouldPrefetchData(clientNavPath, { url: currentLoc }), true);
232-
});
233-
234-
test('valid prefetchUrl', () => {
235-
const clientNavPath = '/about';
236-
const currentLoc = new URL('https://qwik.dev/contact');
237-
assert.equal(shouldPrefetchData(clientNavPath, { url: currentLoc }), true);
238-
});
239-
240-
// shouldPrefetchSymbols.
241191
// ======================
242192
test('missing clientNavPath', () => {
243193
const clientNavPath = null;
244194
const currentLoc = new URL('https://qwik.dev/contact');
245-
assert.equal(shouldPrefetchSymbols(clientNavPath, { url: currentLoc }), false);
195+
assert.equal(shouldPreload(clientNavPath, { url: currentLoc }), false);
246196
});
247197

248198
test('path and current path the same, has different querystring and hash', () => {
249199
const clientNavPath = '/about?qs#hash';
250200
const currentLoc = new URL('https://qwik.dev/about');
251-
assert.equal(shouldPrefetchSymbols(clientNavPath, { url: currentLoc }), false);
201+
assert.equal(shouldPreload(clientNavPath, { url: currentLoc }), false);
252202
});
253203

254204
test('path and current path the same, different trailing slash', () => {
255205
const clientNavPath = '/about/';
256206
const currentLoc = new URL('https://qwik.dev/about');
257-
assert.equal(shouldPrefetchSymbols(clientNavPath, { url: currentLoc }), false);
207+
assert.equal(shouldPreload(clientNavPath, { url: currentLoc }), false);
258208
});
259209

260210
test('path and current path the same', () => {
261211
const clientNavPath = '/about';
262212
const currentLoc = new URL('https://qwik.dev/about');
263-
assert.equal(shouldPrefetchSymbols(clientNavPath, { url: currentLoc }), false);
213+
assert.equal(shouldPreload(clientNavPath, { url: currentLoc }), false);
264214
});
265215

266216
test('valid prefetchUrl, has querystring and hash', () => {
267217
const clientNavPath = '/about?qs#hash';
268218
const currentLoc = new URL('https://qwik.dev/contact');
269-
assert.equal(shouldPrefetchSymbols(clientNavPath, { url: currentLoc }), true);
219+
assert.equal(shouldPreload(clientNavPath, { url: currentLoc }), true);
270220
});
271221

272222
test('valid prefetchUrl, trailing slash', () => {
273223
const clientNavPath = '/about/';
274224
const currentLoc = new URL('https://qwik.dev/contact');
275-
assert.equal(shouldPrefetchSymbols(clientNavPath, { url: currentLoc }), true);
225+
assert.equal(shouldPreload(clientNavPath, { url: currentLoc }), true);
276226
});
277227

278228
test('valid prefetchUrl', () => {
279229
const clientNavPath = '/about';
280230
const currentLoc = new URL('https://qwik.dev/contact');
281-
assert.equal(shouldPrefetchSymbols(clientNavPath, { url: currentLoc }), true);
231+
assert.equal(shouldPreload(clientNavPath, { url: currentLoc }), true);
282232
});

0 commit comments

Comments
 (0)