Skip to content

Commit 58fca91

Browse files
Rich-HarrisRich Harris
and
Rich Harris
authored
Refactor navigate (#9698)
* update props directly inside invalidate * make arguments non-optional * remove update function --------- Co-authored-by: Rich Harris <[email protected]>
1 parent c2b3df7 commit 58fca91

File tree

1 file changed

+160
-195
lines changed

1 file changed

+160
-195
lines changed

packages/kit/src/runtime/client/client.js

+160-195
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,18 @@ export function create_client(app, target) {
162162
// was then triggered and is still running while the invalidation kicks in,
163163
// at which point the invalidation should take over and "win".
164164
load_cache = null;
165-
await update(intent, url, []);
165+
166+
const nav_token = (token = {});
167+
const navigation_result = intent && (await load_route(intent));
168+
if (nav_token !== token) return;
169+
170+
if (navigation_result) {
171+
if (navigation_result.type === 'redirect') {
172+
return goto(new URL(navigation_result.location, url).href, {}, [url.pathname], nav_token);
173+
} else {
174+
root.$set(navigation_result.props);
175+
}
176+
}
166177
}
167178

168179
/** @param {number} index */
@@ -256,183 +267,6 @@ export function create_client(app, target) {
256267
await Promise.all(promises);
257268
}
258269

259-
/**
260-
* Returns `true` if update completes, `false` if it is aborted
261-
* @param {import('./types').NavigationIntent | undefined} intent
262-
* @param {URL} url
263-
* @param {string[]} redirect_chain
264-
* @param {number} [previous_history_index]
265-
* @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts]
266-
* @param {{}} [nav_token] To distinguish between different navigation events and determine the latest. Needed for example for redirects to keep the original token
267-
* @param {() => void} [callback]
268-
*/
269-
async function update(
270-
intent,
271-
url,
272-
redirect_chain,
273-
previous_history_index,
274-
opts,
275-
nav_token = {},
276-
callback
277-
) {
278-
token = nav_token;
279-
let navigation_result = intent && (await load_route(intent));
280-
281-
if (!navigation_result) {
282-
if (is_external_url(url, base)) {
283-
return await native_navigation(url);
284-
}
285-
navigation_result = await server_fallback(
286-
url,
287-
{ id: null },
288-
await handle_error(new Error(`Not found: ${url.pathname}`), {
289-
url,
290-
params: {},
291-
route: { id: null }
292-
}),
293-
404
294-
);
295-
}
296-
297-
// if this is an internal navigation intent, use the normalized
298-
// URL for the rest of the function
299-
url = intent?.url || url;
300-
301-
// abort if user navigated during update
302-
if (token !== nav_token) return false;
303-
304-
if (navigation_result.type === 'redirect') {
305-
if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
306-
navigation_result = await load_root_error_page({
307-
status: 500,
308-
error: await handle_error(new Error('Redirect loop'), {
309-
url,
310-
params: {},
311-
route: { id: null }
312-
}),
313-
url,
314-
route: { id: null }
315-
});
316-
} else {
317-
goto(
318-
new URL(navigation_result.location, url).href,
319-
{},
320-
[...redirect_chain, url.pathname],
321-
nav_token
322-
);
323-
return false;
324-
}
325-
} else if (/** @type {number} */ (navigation_result.props.page?.status) >= 400) {
326-
const updated = await stores.updated.check();
327-
if (updated) {
328-
await native_navigation(url);
329-
}
330-
}
331-
332-
// reset invalidation only after a finished navigation. If there are redirects or
333-
// additional invalidations, they should get the same invalidation treatment
334-
invalidated.length = 0;
335-
force_invalidation = false;
336-
337-
updating = true;
338-
339-
// `previous_history_index` will be undefined for invalidation
340-
if (previous_history_index) {
341-
update_scroll_positions(previous_history_index);
342-
capture_snapshot(previous_history_index);
343-
}
344-
345-
// ensure the url pathname matches the page's trailing slash option
346-
if (
347-
navigation_result.props.page?.url &&
348-
navigation_result.props.page.url.pathname !== url.pathname
349-
) {
350-
url.pathname = navigation_result.props.page?.url.pathname;
351-
}
352-
353-
if (opts && opts.details) {
354-
const { details } = opts;
355-
const change = details.replaceState ? 0 : 1;
356-
details.state[INDEX_KEY] = current_history_index += change;
357-
history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url);
358-
359-
if (!details.replaceState) {
360-
// if we navigated back, then pushed a new state, we can
361-
// release memory by pruning the scroll/snapshot lookup
362-
let i = current_history_index + 1;
363-
while (snapshots[i] || scroll_positions[i]) {
364-
delete snapshots[i];
365-
delete scroll_positions[i];
366-
i += 1;
367-
}
368-
}
369-
}
370-
371-
// reset preload synchronously after the history state has been set to avoid race conditions
372-
load_cache = null;
373-
374-
if (started) {
375-
current = navigation_result.state;
376-
377-
// reset url before updating page store
378-
if (navigation_result.props.page) {
379-
navigation_result.props.page.url = url;
380-
}
381-
382-
root.$set(navigation_result.props);
383-
} else {
384-
initialize(navigation_result);
385-
}
386-
387-
// opts must be passed if we're navigating
388-
if (opts) {
389-
const { scroll, keepfocus } = opts;
390-
const { activeElement } = document;
391-
392-
// need to render the DOM before we can scroll to the rendered elements and do focus management
393-
await tick();
394-
395-
// we reset scroll before dealing with focus, to avoid a flash of unscrolled content
396-
if (autoscroll) {
397-
const deep_linked =
398-
url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
399-
if (scroll) {
400-
scrollTo(scroll.x, scroll.y);
401-
} else if (deep_linked) {
402-
// Here we use `scrollIntoView` on the element instead of `scrollTo`
403-
// because it natively supports the `scroll-margin` and `scroll-behavior`
404-
// CSS properties.
405-
deep_linked.scrollIntoView();
406-
} else {
407-
scrollTo(0, 0);
408-
}
409-
}
410-
411-
const changed_focus =
412-
// reset focus only if any manual focus management didn't override it
413-
document.activeElement !== activeElement &&
414-
// also refocus when activeElement is body already because the
415-
// focus event might not have been fired on it yet
416-
document.activeElement !== document.body;
417-
418-
if (!keepfocus && !changed_focus) {
419-
await reset_focus();
420-
}
421-
} else {
422-
await tick();
423-
}
424-
425-
autoscroll = true;
426-
427-
if (navigation_result.props.page) {
428-
page = navigation_result.props.page;
429-
}
430-
431-
if (callback) callback();
432-
433-
updating = false;
434-
}
435-
436270
/** @param {import('./types').NavigationFinished} result */
437271
function initialize(result) {
438272
if (DEV && document.querySelector('vite-error-overlay')) return;
@@ -1131,7 +965,7 @@ export function create_client(app, target) {
1131965
details,
1132966
type,
1133967
delta,
1134-
nav_token,
968+
nav_token = {},
1135969
accepted,
1136970
blocked
1137971
}) {
@@ -1154,25 +988,156 @@ export function create_client(app, target) {
1154988
stores.navigating.set(navigation);
1155989
}
1156990

1157-
await update(
1158-
intent,
1159-
url,
1160-
redirect_chain,
1161-
previous_history_index,
1162-
{
1163-
scroll,
1164-
keepfocus,
1165-
details
1166-
},
1167-
nav_token,
1168-
() => {
1169-
navigating = false;
1170-
callbacks.after_navigate.forEach((fn) =>
1171-
fn(/** @type {import('types').AfterNavigate} */ (navigation))
991+
token = nav_token;
992+
let navigation_result = intent && (await load_route(intent));
993+
994+
if (!navigation_result) {
995+
if (is_external_url(url, base)) {
996+
return await native_navigation(url);
997+
}
998+
navigation_result = await server_fallback(
999+
url,
1000+
{ id: null },
1001+
await handle_error(new Error(`Not found: ${url.pathname}`), {
1002+
url,
1003+
params: {},
1004+
route: { id: null }
1005+
}),
1006+
404
1007+
);
1008+
}
1009+
1010+
// if this is an internal navigation intent, use the normalized
1011+
// URL for the rest of the function
1012+
url = intent?.url || url;
1013+
1014+
// abort if user navigated during update
1015+
if (token !== nav_token) return false;
1016+
1017+
if (navigation_result.type === 'redirect') {
1018+
if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
1019+
navigation_result = await load_root_error_page({
1020+
status: 500,
1021+
error: await handle_error(new Error('Redirect loop'), {
1022+
url,
1023+
params: {},
1024+
route: { id: null }
1025+
}),
1026+
url,
1027+
route: { id: null }
1028+
});
1029+
} else {
1030+
goto(
1031+
new URL(navigation_result.location, url).href,
1032+
{},
1033+
[...redirect_chain, url.pathname],
1034+
nav_token
11721035
);
1173-
stores.navigating.set(null);
1036+
return false;
1037+
}
1038+
} else if (/** @type {number} */ (navigation_result.props.page?.status) >= 400) {
1039+
const updated = await stores.updated.check();
1040+
if (updated) {
1041+
await native_navigation(url);
11741042
}
1043+
}
1044+
1045+
// reset invalidation only after a finished navigation. If there are redirects or
1046+
// additional invalidations, they should get the same invalidation treatment
1047+
invalidated.length = 0;
1048+
force_invalidation = false;
1049+
1050+
updating = true;
1051+
1052+
update_scroll_positions(previous_history_index);
1053+
capture_snapshot(previous_history_index);
1054+
1055+
// ensure the url pathname matches the page's trailing slash option
1056+
if (
1057+
navigation_result.props.page?.url &&
1058+
navigation_result.props.page.url.pathname !== url.pathname
1059+
) {
1060+
url.pathname = navigation_result.props.page?.url.pathname;
1061+
}
1062+
1063+
if (details) {
1064+
const change = details.replaceState ? 0 : 1;
1065+
details.state[INDEX_KEY] = current_history_index += change;
1066+
history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url);
1067+
1068+
if (!details.replaceState) {
1069+
// if we navigated back, then pushed a new state, we can
1070+
// release memory by pruning the scroll/snapshot lookup
1071+
let i = current_history_index + 1;
1072+
while (snapshots[i] || scroll_positions[i]) {
1073+
delete snapshots[i];
1074+
delete scroll_positions[i];
1075+
i += 1;
1076+
}
1077+
}
1078+
}
1079+
1080+
// reset preload synchronously after the history state has been set to avoid race conditions
1081+
load_cache = null;
1082+
1083+
if (started) {
1084+
current = navigation_result.state;
1085+
1086+
// reset url before updating page store
1087+
if (navigation_result.props.page) {
1088+
navigation_result.props.page.url = url;
1089+
}
1090+
1091+
root.$set(navigation_result.props);
1092+
} else {
1093+
initialize(navigation_result);
1094+
}
1095+
1096+
const { activeElement } = document;
1097+
1098+
// need to render the DOM before we can scroll to the rendered elements and do focus management
1099+
await tick();
1100+
1101+
// we reset scroll before dealing with focus, to avoid a flash of unscrolled content
1102+
if (autoscroll) {
1103+
const deep_linked =
1104+
url.hash && document.getElementById(decodeURIComponent(url.hash.slice(1)));
1105+
if (scroll) {
1106+
scrollTo(scroll.x, scroll.y);
1107+
} else if (deep_linked) {
1108+
// Here we use `scrollIntoView` on the element instead of `scrollTo`
1109+
// because it natively supports the `scroll-margin` and `scroll-behavior`
1110+
// CSS properties.
1111+
deep_linked.scrollIntoView();
1112+
} else {
1113+
scrollTo(0, 0);
1114+
}
1115+
}
1116+
1117+
const changed_focus =
1118+
// reset focus only if any manual focus management didn't override it
1119+
document.activeElement !== activeElement &&
1120+
// also refocus when activeElement is body already because the
1121+
// focus event might not have been fired on it yet
1122+
document.activeElement !== document.body;
1123+
1124+
if (!keepfocus && !changed_focus) {
1125+
await reset_focus();
1126+
}
1127+
1128+
autoscroll = true;
1129+
1130+
if (navigation_result.props.page) {
1131+
page = navigation_result.props.page;
1132+
}
1133+
1134+
navigating = false;
1135+
callbacks.after_navigate.forEach((fn) =>
1136+
fn(/** @type {import('types').AfterNavigate} */ (navigation))
11751137
);
1138+
stores.navigating.set(null);
1139+
1140+
updating = false;
11761141
}
11771142

11781143
/**

0 commit comments

Comments
 (0)