Skip to content

Commit bd99b63

Browse files
committed
feat: allow to control behavior on hydration error
closes #13519
1 parent 3cf2b77 commit bd99b63

File tree

7 files changed

+52
-14
lines changed

7 files changed

+52
-14
lines changed

.changeset/orange-apes-change.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': minor
3+
---
4+
5+
feat: allow to control behavior on hydration error

packages/kit/src/core/config/options.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,8 @@ const options = object(
262262

263263
router: object({
264264
type: list(['pathname', 'hash']),
265-
resolution: list(['client', 'server'])
265+
resolution: list(['client', 'server']),
266+
hydrationErrorHandling: list(['error', 'keep html'])
266267
}),
267268

268269
serviceWorker: object({

packages/kit/src/exports/public.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,16 @@ export interface KitConfig {
681681
* @since 2.17.0
682682
*/
683683
resolution?: 'client' | 'server';
684+
/**
685+
* Sometimes the server renders the HTML successfully, but during hydration on the client something goes wrong. One example is a flaky network where
686+
* the request for one of the JavaScript files that is needed for a page to work fails. This option allows you to configure what should happen in such a case:
687+
* - `'error'` (default) - the client will fall back to the root error page. The user will immediately see something's off and depending on the error page can act accordingly. Use this if your app is too sensitive to not-immediately-visible broken states.
688+
* - `'keep html'` - the client will show the HTML that was rendered on the server and not attempt to hydrate the page. The user will see the successful server-rendered page, but no interactivity will be available and navigations are full page reloads. Use this if your app can largely function without JavaScript.
689+
*
690+
* @default "error"
691+
* @since 2.18.0
692+
*/
693+
hydrationErrorHandling?: 'error' | 'keep html';
684694
};
685695
serviceWorker?: {
686696
/**

packages/kit/src/exports/vite/index.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,9 @@ async function kit({ svelte_config }) {
321321
__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: s(kit.version.pollInterval),
322322
__SVELTEKIT_DEV__: 'false',
323323
__SVELTEKIT_EMBEDDED__: kit.embedded ? 'true' : 'false',
324-
__SVELTEKIT_CLIENT_ROUTING__: kit.router.resolution === 'client' ? 'true' : 'false'
324+
__SVELTEKIT_CLIENT_ROUTING__: kit.router.resolution === 'client' ? 'true' : 'false',
325+
__SVELTEKIT_NO_ROOT_ERROR_ON_HYDRATION__:
326+
kit.router.hydrationErrorHandling === 'keep html'
325327
};
326328

327329
if (!secondary_build_started) {
@@ -332,7 +334,9 @@ async function kit({ svelte_config }) {
332334
__SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0',
333335
__SVELTEKIT_DEV__: 'true',
334336
__SVELTEKIT_EMBEDDED__: kit.embedded ? 'true' : 'false',
335-
__SVELTEKIT_CLIENT_ROUTING__: kit.router.resolution === 'client' ? 'true' : 'false'
337+
__SVELTEKIT_CLIENT_ROUTING__: kit.router.resolution === 'client' ? 'true' : 'false',
338+
__SVELTEKIT_NO_ROOT_ERROR_ON_HYDRATION__:
339+
kit.router.hydrationErrorHandling === 'keep html'
336340
};
337341

338342
// These Kit dependencies are packaged as CommonJS, which means they must always be externalized.

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

+17-11
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ let current = {
212212
/** this being true means we SSR'd */
213213
let hydrated = false;
214214
let started = false;
215+
let errored_on_hydration = false;
215216
let autoscroll = true;
216217
let updating = false;
217218
let is_navigating = false;
@@ -1242,7 +1243,7 @@ function get_rerouted_url(url) {
12421243
* @returns {Promise<import('./types.js').NavigationIntent | undefined>}
12431244
*/
12441245
async function get_navigation_intent(url, invalidating) {
1245-
if (!url) return;
1246+
if (!url || errored_on_hydration) return;
12461247
if (is_external_url(url, base, app.hash)) return;
12471248

12481249
if (__SVELTEKIT_CLIENT_ROUTING__) {
@@ -1678,7 +1679,7 @@ function setup_preload() {
16781679
*/
16791680
async function preload(element, priority) {
16801681
const a = find_anchor(element, container);
1681-
if (!a || a === current_a) return;
1682+
if (!a || a === current_a || errored_on_hydration) return;
16821683

16831684
const { url, external, download } = get_link_info(a, base, app.hash);
16841685
if (external || download) return;
@@ -2213,7 +2214,7 @@ function _start_router() {
22132214
if (event.defaultPrevented) return;
22142215

22152216
const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container);
2216-
if (!a) return;
2217+
if (!a || errored_on_hydration) return;
22172218

22182219
const { url, external, target, download } = get_link_info(a, base, app.hash);
22192220
if (!url) return;
@@ -2584,15 +2585,20 @@ async function _hydrate(
25842585
return;
25852586
}
25862587

2587-
result = await load_root_error_page({
2588-
status: get_status(error),
2589-
error: await handle_error(error, { url, params, route }),
2590-
url,
2591-
route
2592-
});
2588+
if (__SVELTEKIT_NO_ROOT_ERROR_ON_HYDRATION__) {
2589+
errored_on_hydration = true;
2590+
return;
2591+
} else {
2592+
result = await load_root_error_page({
2593+
status: get_status(error),
2594+
error: await handle_error(error, { url, params, route }),
2595+
url,
2596+
route
2597+
});
25932598

2594-
target.textContent = '';
2595-
hydrate = false;
2599+
target.textContent = '';
2600+
hydrate = false;
2601+
}
25962602
}
25972603

25982604
if (result.props.page) {

packages/kit/src/types/global-private.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ declare global {
66
const __SVELTEKIT_EMBEDDED__: boolean;
77
/** True if `config.kit.router.resolution === 'client'` */
88
const __SVELTEKIT_CLIENT_ROUTING__: boolean;
9+
/** True if `config.kit.router.hydrationErrorHandling === 'keep html'` */
10+
const __SVELTEKIT_NO_ROOT_ERROR_ON_HYDRATION__: boolean;
911
/**
1012
* This makes the use of specific features visible at both dev and build time, in such a
1113
* way that we can error when they are not supported by the target platform.

packages/kit/types/index.d.ts

+10
Original file line numberDiff line numberDiff line change
@@ -663,6 +663,16 @@ declare module '@sveltejs/kit' {
663663
* @since 2.17.0
664664
*/
665665
resolution?: 'client' | 'server';
666+
/**
667+
* Sometimes the server renders the HTML successfully, but during hydration on the client something goes wrong. One example is a flaky network where
668+
* the request for one of the JavaScript files that is needed for a page to work fails. This option allows you to configure what should happen in such a case:
669+
* - `'error'` (default) - the client will fall back to the root error page. The user will immediately see something's off and depending on the error page can act accordingly. Use this if your app is too sensitive to not-immediately-visible broken states.
670+
* - `'keep html'` - the client will show the HTML that was rendered on the server and not attempt to hydrate the page. The user will see the successful server-rendered page, but no interactivity will be available and navigations are full page reloads. Use this if your app can largely function without JavaScript.
671+
*
672+
* @default "error"
673+
* @since 2.18.0
674+
*/
675+
hydrationErrorHandling?: 'error' | 'keep html';
666676
};
667677
serviceWorker?: {
668678
/**

0 commit comments

Comments
 (0)