diff --git a/.changeset/orange-apes-change.md b/.changeset/orange-apes-change.md new file mode 100644 index 000000000000..05f3a3c60758 --- /dev/null +++ b/.changeset/orange-apes-change.md @@ -0,0 +1,5 @@ +--- +'@sveltejs/kit': minor +--- + +feat: allow to control behavior on hydration error diff --git a/packages/kit/src/core/config/index.spec.js b/packages/kit/src/core/config/index.spec.js index 9c577f5425c0..368b25f802c4 100644 --- a/packages/kit/src/core/config/index.spec.js +++ b/packages/kit/src/core/config/index.spec.js @@ -96,7 +96,8 @@ const get_defaults = (prefix = '') => ({ outDir: join(prefix, '.svelte-kit'), router: { type: 'pathname', - resolution: 'client' + resolution: 'client', + hydrationErrorHandling: 'error' }, serviceWorker: { register: true diff --git a/packages/kit/src/core/config/options.js b/packages/kit/src/core/config/options.js index a2b9bb81759d..85725e2b4cf2 100644 --- a/packages/kit/src/core/config/options.js +++ b/packages/kit/src/core/config/options.js @@ -262,7 +262,8 @@ const options = object( router: object({ type: list(['pathname', 'hash']), - resolution: list(['client', 'server']) + resolution: list(['client', 'server']), + hydrationErrorHandling: list(['error', 'keep html']) }), serviceWorker: object({ diff --git a/packages/kit/src/exports/public.d.ts b/packages/kit/src/exports/public.d.ts index f25cc225e194..f2ac46696c8b 100644 --- a/packages/kit/src/exports/public.d.ts +++ b/packages/kit/src/exports/public.d.ts @@ -681,6 +681,16 @@ export interface KitConfig { * @since 2.17.0 */ resolution?: 'client' | 'server'; + /** + * Sometimes the server renders the HTML successfully, but during hydration on the client something goes wrong. One example is a flaky network where + * 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: + * - `'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. + * - `'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. + * + * @default "error" + * @since 2.18.0 + */ + hydrationErrorHandling?: 'error' | 'keep html'; }; serviceWorker?: { /** diff --git a/packages/kit/src/exports/vite/index.js b/packages/kit/src/exports/vite/index.js index bdb37b1f9cff..8b51d5325a1f 100644 --- a/packages/kit/src/exports/vite/index.js +++ b/packages/kit/src/exports/vite/index.js @@ -321,7 +321,9 @@ async function kit({ svelte_config }) { __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: s(kit.version.pollInterval), __SVELTEKIT_DEV__: 'false', __SVELTEKIT_EMBEDDED__: kit.embedded ? 'true' : 'false', - __SVELTEKIT_CLIENT_ROUTING__: kit.router.resolution === 'client' ? 'true' : 'false' + __SVELTEKIT_CLIENT_ROUTING__: kit.router.resolution === 'client' ? 'true' : 'false', + __SVELTEKIT_NO_ROOT_ERROR_ON_HYDRATION__: + kit.router.hydrationErrorHandling === 'keep html' }; if (!secondary_build_started) { @@ -332,7 +334,9 @@ async function kit({ svelte_config }) { __SVELTEKIT_APP_VERSION_POLL_INTERVAL__: '0', __SVELTEKIT_DEV__: 'true', __SVELTEKIT_EMBEDDED__: kit.embedded ? 'true' : 'false', - __SVELTEKIT_CLIENT_ROUTING__: kit.router.resolution === 'client' ? 'true' : 'false' + __SVELTEKIT_CLIENT_ROUTING__: kit.router.resolution === 'client' ? 'true' : 'false', + __SVELTEKIT_NO_ROOT_ERROR_ON_HYDRATION__: + kit.router.hydrationErrorHandling === 'keep html' }; // These Kit dependencies are packaged as CommonJS, which means they must always be externalized. diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js index 630f035754a1..16cd69a65de6 100644 --- a/packages/kit/src/runtime/client/client.js +++ b/packages/kit/src/runtime/client/client.js @@ -212,6 +212,7 @@ let current = { /** this being true means we SSR'd */ let hydrated = false; let started = false; +let errored_on_hydration = false; let autoscroll = true; let updating = false; let is_navigating = false; @@ -1242,7 +1243,7 @@ function get_rerouted_url(url) { * @returns {Promise} */ async function get_navigation_intent(url, invalidating) { - if (!url) return; + if (!url || errored_on_hydration) return; if (is_external_url(url, base, app.hash)) return; if (__SVELTEKIT_CLIENT_ROUTING__) { @@ -1678,7 +1679,7 @@ function setup_preload() { */ async function preload(element, priority) { const a = find_anchor(element, container); - if (!a || a === current_a) return; + if (!a || a === current_a || errored_on_hydration) return; const { url, external, download } = get_link_info(a, base, app.hash); if (external || download) return; @@ -2213,7 +2214,7 @@ function _start_router() { if (event.defaultPrevented) return; const a = find_anchor(/** @type {Element} */ (event.composedPath()[0]), container); - if (!a) return; + if (!a || errored_on_hydration) return; const { url, external, target, download } = get_link_info(a, base, app.hash); if (!url) return; @@ -2584,15 +2585,20 @@ async function _hydrate( return; } - result = await load_root_error_page({ - status: get_status(error), - error: await handle_error(error, { url, params, route }), - url, - route - }); + if (__SVELTEKIT_NO_ROOT_ERROR_ON_HYDRATION__) { + errored_on_hydration = true; + return; + } else { + result = await load_root_error_page({ + status: get_status(error), + error: await handle_error(error, { url, params, route }), + url, + route + }); - target.textContent = ''; - hydrate = false; + target.textContent = ''; + hydrate = false; + } } if (result.props.page) { diff --git a/packages/kit/src/types/global-private.d.ts b/packages/kit/src/types/global-private.d.ts index ab659a8db5c5..4a368eab6008 100644 --- a/packages/kit/src/types/global-private.d.ts +++ b/packages/kit/src/types/global-private.d.ts @@ -6,6 +6,8 @@ declare global { const __SVELTEKIT_EMBEDDED__: boolean; /** True if `config.kit.router.resolution === 'client'` */ const __SVELTEKIT_CLIENT_ROUTING__: boolean; + /** True if `config.kit.router.hydrationErrorHandling === 'keep html'` */ + const __SVELTEKIT_NO_ROOT_ERROR_ON_HYDRATION__: boolean; /** * This makes the use of specific features visible at both dev and build time, in such a * way that we can error when they are not supported by the target platform. diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index f7ca3bce33f9..5732b2da0937 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -663,6 +663,16 @@ declare module '@sveltejs/kit' { * @since 2.17.0 */ resolution?: 'client' | 'server'; + /** + * Sometimes the server renders the HTML successfully, but during hydration on the client something goes wrong. One example is a flaky network where + * 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: + * - `'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. + * - `'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. + * + * @default "error" + * @since 2.18.0 + */ + hydrationErrorHandling?: 'error' | 'keep html'; }; serviceWorker?: { /**