Skip to content
Open
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/big-masks-shave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': minor
---

feat: `hydratable` API
107 changes: 107 additions & 0 deletions documentation/docs/06-runtime/05-hydratable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
title: Hydratable data
---

In Svelte, when you want to render asynchonous content data on the server, you can simply `await` it. This is great! However, it comes with a major pitall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes:

```svelte
<script>
import { getUser } from 'my-database-library';

// This will get the user on the server, render the user's name into the h1,
// and then, during hydration on the client, it will get the user _again_,
// blocking hydration until it's done.
const user = await getUser();
</script>

<h1>{user.name}</h1>
```

That's silly, though. If we've already done the hard work of getting the data on the server, we don't want to get it again during hydration on the client. `hydratable` is a low-level API build to solve this problem. You probably won't need this very often -- it will probably be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).

To fix the example above:

```svelte
<script>
import { hydratable } from 'svelte';
import { getUser } from 'my-database-library';

// During server rendering, this will serialize and stash the result of `getUser`, associating
// it with the provided key and baking it into the `head` content. During hydration, it will
// look for the serialized version, returning it instead of running `getUser`. After hydration
// is done, if it's called again, it'll simply invoke `getUser`.
const user = await hydratable('user', getUser());
</script>

<h1>{user.name}</h1>
```

This API can also be used to provide access to random or time-based values that are stable between server rendering and hydration. For example, to get a random number that doesn't update on hydration:

```ts
import { hydratable } from 'svelte';
const rand = hydratable('random', () => Math.random());
```

If you're a library author, be sure to prefix the keys of your `hydratable` values with the name of your library so that your keys don't conflict with other libraries.

## Imperative API

If you're writing a library with separate server and client exports, it may be more convenient to use the imperative API:

```ts
import { hydratable } from 'svelte';

const value = hydratable.get('foo'); // only works on the client
const hasValue = hydratable.has('foo');
hydratable.set('foo', 'whatever value you want'); // only works on the server
```

## Custom serialization

By default, Svelte uses [`devalue`](https://npmjs.com/package/devalue) to serialize your data on the server so that decoding it on the client requires no dependencies. If you need to serialize additional things not covered by `devalue`, you can provide your own transport mechanisms by writing custom `encode` and `decode` methods.

### `encode`

Encode receives a value and outputs _the JavaScript code necessary to create that value on the client_. For example, Svelte's built-in encoder looks like this:

```js
import * as devalue from 'devalue';

/**
* @param {any} value
*/
function encode (value) {
return devalue.uneval(value);
}

encode(['hello', 'world']); // outputs `['hello', 'world']`
```

### `decode`

`decode` accepts whatever the JavaScript that `encode` outputs resolves to, and returns whatever the final value from `hydratable` should be.

### Usage

When using the isomorphic API, you must provide either `encode` or `decode`, depending on the environment. This enables your bundler to treeshake the unneeded code during your build:

```svelte
<script>
import { hydratable } from 'svelte';
import { BROWSER } from 'esm-env';
import { encode, decode } from '$lib/encoders';

const random = hydratable('random', () => Math.random(), { transport: BROWSER ? { decode } : { encode }});
</script>
```

For the imperative API, you just provide `encode` or `decode` depending on which method you're using:

```ts
import { hydratable } from 'svelte';
import { encode, decode } from '$lib/encoders';

const random = hydratable.get('random', { decode });
hydratable.set('random', Math.random(), { encode });
```
29 changes: 23 additions & 6 deletions documentation/docs/98-reference/.generated/client-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,6 @@ $effect(() => {

Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.

### experimental_async_fork

```
Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
```

### flush_sync_in_effect

```
Expand All @@ -146,6 +140,12 @@ The `flushSync()` function can be used to flush any pending effects synchronousl

This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.

### fn_unavailable_on_client

```
`%name%`(...) is unavailable in the browser.
```

### fork_discarded

```
Expand All @@ -164,6 +164,23 @@ Cannot create a fork inside an effect or when state changes are pending
`getAbortSignal()` can only be called inside an effect or derived
```

### hydratable_missing_but_expected_e

```
Expected to find a hydratable with key `%key%` during hydration, but did not.
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
```

if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```

### hydration_failed

```
Expand Down
17 changes: 17 additions & 0 deletions documentation/docs/98-reference/.generated/client-warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,23 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
%handler% should be a function. Did you mean to %suggestion%?
```

### hydratable_missing_but_expected_w

```
Expected to find a hydratable with key `%key%` during hydration, but did not.
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';
```

if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```
### hydration_attribute_changed
```
Expand Down
35 changes: 35 additions & 0 deletions documentation/docs/98-reference/.generated/server-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,51 @@ Encountered asynchronous work while rendering synchronously.

You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.

### fn_unavailable_on_server

```
`%name%`(...) is unavailable on the server.
```

### html_deprecated

```
The `html` property of server render results has been deprecated. Use `body` instead.
```

### hydratable_clobbering

```
Attempted to set hydratable with key `%key%` twice. This behavior is undefined.

First set occurred at:
%stack%
```

This error occurs when using `hydratable` or `hydratable.set` multiple times with the same key. To avoid this, you can combine `hydratable` with `cache`, or check whether the value has already been set with `hydratable.has`.

```svelte
<script>
import { hydratable } from 'svelte';
await Promise.all([
// which one should "win" and be serialized in the rendered response?
hydratable('hello', () => 'world'),
hydratable('hello', () => 'dad')
])
</script>
```

### lifecycle_function_unavailable

```
`%name%(...)` is not available on the server
```

Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

### render_context_unavailable

```
Failed to retrieve `render` context. %addendum%
If `AsyncLocalStorage` is available, you're likely calling a function that needs access to the `render` context (`hydratable`, `cache`, or something that depends on these) from outside of `render`. If `AsyncLocalStorage` is not available, these functions must also be called synchronously from within `render` -- i.e. not after any `await`s.
```
6 changes: 6 additions & 0 deletions documentation/docs/98-reference/.generated/shared-errors.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->

### experimental_async_required

```
Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
```

### invalid_default_snippet

```
Expand Down
23 changes: 19 additions & 4 deletions packages/svelte/messages/client-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,6 @@ $effect(() => {

Often when encountering this issue, the value in question shouldn't be state (for example, if you are pushing to a `logs` array in an effect, make `logs` a normal array rather than `$state([])`). In the rare cases where you really _do_ need to write to state in an effect — [which you should avoid]($effect#When-not-to-use-$effect) — you can read the state with [untrack](svelte#untrack) to avoid adding it as a dependency.

## experimental_async_fork

> Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`

## flush_sync_in_effect

> Cannot use `flushSync` inside an effect
Expand All @@ -112,6 +108,10 @@ The `flushSync()` function can be used to flush any pending effects synchronousl

This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.

## fn_unavailable_on_client

> `%name%`(...) is unavailable in the browser.

## fork_discarded

> Cannot commit a fork that was already discarded
Expand All @@ -124,6 +124,21 @@ This restriction only applies when using the `experimental.async` option, which

> `getAbortSignal()` can only be called inside an effect or derived

## hydratable_missing_but_expected_e

> Expected to find a hydratable with key `%key%` during hydration, but did not.
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';

if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```

## hydration_failed

> Failed to hydrate the application
Expand Down
15 changes: 15 additions & 0 deletions packages/svelte/messages/client-warnings/warnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`

> %handler% should be a function. Did you mean to %suggestion%?

## hydratable_missing_but_expected_w

> Expected to find a hydratable with key `%key%` during hydration, but did not.
This can happen if you render a hydratable on the client that was not rendered on the server, and means that it was forced to fall back to running its function blockingly during hydration. This is bad for performance, as it blocks hydration until the asynchronous work completes.
```svelte
<script>
import { hydratable } from 'svelte';

if (BROWSER) {
// bad! nothing can become interactive until this asynchronous work is done
await hydratable('foo', get_slow_random_number);
}
</script>
```

## hydration_attribute_changed

> The `%attribute%` attribute on `%html%` changed its value between server and client renders. The client value, `%value%`, will be ignored in favour of the server value
Expand Down
29 changes: 29 additions & 0 deletions packages/svelte/messages/server-errors/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,41 @@

You (or the framework you're using) called [`render(...)`](svelte-server#render) with a component containing an `await` expression. Either `await` the result of `render` or wrap the `await` (or the component containing it) in a [`<svelte:boundary>`](svelte-boundary) with a `pending` snippet.

## fn_unavailable_on_server

> `%name%`(...) is unavailable on the server.

## html_deprecated

> The `html` property of server render results has been deprecated. Use `body` instead.

## hydratable_clobbering

> Attempted to set hydratable with key `%key%` twice. This behavior is undefined.
>
> First set occurred at:
> %stack%

This error occurs when using `hydratable` or `hydratable.set` multiple times with the same key. To avoid this, you can combine `hydratable` with `cache`, or check whether the value has already been set with `hydratable.has`.

```svelte
<script>
import { hydratable } from 'svelte';
await Promise.all([
// which one should "win" and be serialized in the rendered response?
hydratable('hello', () => 'world'),
hydratable('hello', () => 'dad')
])
</script>
```

## lifecycle_function_unavailable

> `%name%(...)` is not available on the server

Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.

## render_context_unavailable

> Failed to retrieve `render` context. %addendum%
If `AsyncLocalStorage` is available, you're likely calling a function that needs access to the `render` context (`hydratable`, `cache`, or something that depends on these) from outside of `render`. If `AsyncLocalStorage` is not available, these functions must also be called synchronously from within `render` -- i.e. not after any `await`s.
4 changes: 4 additions & 0 deletions packages/svelte/messages/shared-errors/errors.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## experimental_async_required

> Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
## invalid_default_snippet

> Cannot use `{@render children(...)}` if the parent component uses `let:` directives. Consider using a named snippet instead
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"devalue": "^5.5.0",
"esm-env": "^1.2.1",
"esrap": "^2.1.0",
"is-reference": "^3.0.3",
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/index-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ export {
hasContext,
setContext
} from './internal/client/context.js';
export { hydratable } from './internal/client/hydratable.js';
export { hydrate, mount, unmount } from './internal/client/render.js';
export { tick, untrack, settled } from './internal/client/runtime.js';
export { createRawSnippet } from './internal/client/dom/blocks/snippet.js';
2 changes: 2 additions & 0 deletions packages/svelte/src/index-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,6 @@ export {
setContext
} from './internal/server/context.js';

export { hydratable } from './internal/server/hydratable.js';

export { createRawSnippet } from './internal/server/blocks/snippet.js';
1 change: 1 addition & 0 deletions packages/svelte/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,3 +369,4 @@ export interface Fork {
}

export * from './index-client.js';
export { Hydratable, Transport, Encode, Decode } from '#shared';
Loading
Loading