Skip to content

feat: allow to control behavior on hydration error #13521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/orange-apes-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/kit': minor
---

feat: allow to control behavior on hydration error
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/kit/src/core/config/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
10 changes: 10 additions & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
/**
Expand Down
8 changes: 6 additions & 2 deletions packages/kit/src/exports/vite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
28 changes: 17 additions & 11 deletions packages/kit/src/runtime/client/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1242,7 +1243,7 @@ function get_rerouted_url(url) {
* @returns {Promise<import('./types.js').NavigationIntent | undefined>}
*/
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__) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will the client know that this has happened and the page is not hydrated?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, good point. We probably also want the await handle_error(error, { url, params, route }) call as before so that normal error reporting if any does work for this error

return;
} else {
Comment on lines +2590 to +2591
Copy link

@itssumitrai itssumitrai Feb 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: since we return here, we don't need the else here

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) {
Expand Down
2 changes: 2 additions & 0 deletions packages/kit/src/types/global-private.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions packages/kit/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
/**
Expand Down
Loading