Skip to content

Commit c2a110c

Browse files
elliott-with-the-longest-name-on-githubRich-Harriskaysef
authored
feat: hydratable (#17154)
* feat: * doc comments * types * types * changeset * tests * docs * hopefully * lint * finally figured out test issues * get docs building * the easy stuff * prune errors * feat: capture clobbering better, capture unused keys, don't block on unused keys * progress on serializing nested promises * fix * idk man but the tests are passing so i'ma checkpoint this * fix tests * compare resolved serialized values * robustify * thunkify * fixes * ajsldkfjalsdfkjasd * tests * docs * ugh * ugh ugh ugh * Update documentation/docs/06-runtime/05-hydratable.md Co-authored-by: kaysef <[email protected]> * make errors better * tweak * remove context restoration * fix * trim * simplify hydratable (#17230) * simplify hydratable * tidy up * unused --------- Co-authored-by: Elliott Johnson <[email protected]> * improve errors * include all non-internal frames, handle identical stacks * tweak example * tweak message — setTimeout is covered by ALS * cut out the middleman * unused * tidy * Apply suggestions from code review --------- Co-authored-by: Rich Harris <[email protected]> Co-authored-by: kaysef <[email protected]>
1 parent 129c408 commit c2a110c

File tree

60 files changed

+1202
-94
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+1202
-94
lines changed

.changeset/big-masks-shave.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': minor
3+
---
4+
5+
feat: `hydratable` API
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
---
2+
title: Hydratable data
3+
---
4+
5+
In Svelte, when you want to render asynchronous content data on the server, you can simply `await` it. This is great! However, it comes with a pitfall: when hydrating that content on the client, Svelte has to redo the asynchronous work, which blocks hydration for however long it takes:
6+
7+
```svelte
8+
<script>
9+
import { getUser } from 'my-database-library';
10+
11+
// This will get the user on the server, render the user's name into the h1,
12+
// and then, during hydration on the client, it will get the user _again_,
13+
// blocking hydration until it's done.
14+
const user = await getUser();
15+
</script>
16+
17+
<h1>{user.name}</h1>
18+
```
19+
20+
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 built to solve this problem. You probably won't need this very often -- it will be used behind the scenes by whatever datafetching library you use. For example, it powers [remote functions in SvelteKit](/docs/kit/remote-functions).
21+
22+
To fix the example above:
23+
24+
```svelte
25+
<script>
26+
import { hydratable } from 'svelte';
27+
import { getUser } from 'my-database-library';
28+
29+
// During server rendering, this will serialize and stash the result of `getUser`, associating
30+
// it with the provided key and baking it into the `head` content. During hydration, it will
31+
// look for the serialized version, returning it instead of running `getUser`. After hydration
32+
// is done, if it's called again, it'll simply invoke `getUser`.
33+
const user = await hydratable('user', () => getUser());
34+
</script>
35+
36+
<h1>{user.name}</h1>
37+
```
38+
39+
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:
40+
41+
```ts
42+
import { hydratable } from 'svelte';
43+
const rand = hydratable('random', () => Math.random());
44+
```
45+
46+
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.
47+
48+
## Serialization
49+
50+
All data returned from a `hydratable` function must be serializable. But this doesn't mean you're limited to JSON — Svelte uses [`devalue`](https://npmjs.com/package/devalue), which can serialize all sorts of things including `Map`, `Set`, `URL`, and `BigInt`. Check the documentation page for a full list. In addition to these, thanks to some Svelte magic, you can also fearlessly use promises:
51+
52+
```svelte
53+
<script>
54+
import { hydratable } from 'svelte';
55+
const promises = hydratable('random', () => {
56+
return {
57+
one: Promise.resolve(1),
58+
two: Promise.resolve(2)
59+
}
60+
});
61+
</script>
62+
63+
{await promises.one}
64+
{await promises.two}
65+
```

documentation/docs/98-reference/.generated/client-errors.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,12 +130,16 @@ $effect(() => {
130130

131131
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.
132132

133-
### experimental_async_fork
133+
### flush_sync_in_effect
134134

135135
```
136-
Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
136+
Cannot use `flushSync` inside an effect
137137
```
138138

139+
The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
140+
141+
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
142+
139143
### fork_discarded
140144

141145
```
@@ -154,6 +158,25 @@ Cannot create a fork inside an effect or when state changes are pending
154158
`getAbortSignal()` can only be called inside an effect or derived
155159
```
156160

161+
### hydratable_missing_but_required
162+
163+
```
164+
Expected to find a hydratable with key `%key%` during hydration, but did not.
165+
```
166+
167+
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.
168+
169+
```svelte
170+
<script>
171+
import { hydratable } from 'svelte';
172+
173+
if (BROWSER) {
174+
// bad! nothing can become interactive until this asynchronous work is done
175+
await hydratable('foo', get_slow_random_number);
176+
}
177+
</script>
178+
```
179+
157180
### hydration_failed
158181

159182
```

documentation/docs/98-reference/.generated/client-warnings.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,25 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
140140
%handler% should be a function. Did you mean to %suggestion%?
141141
```
142142

143+
### hydratable_missing_but_expected
144+
145+
```
146+
Expected to find a hydratable with key `%key%` during hydration, but did not.
147+
```
148+
149+
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.
150+
151+
```svelte
152+
<script>
153+
import { hydratable } from 'svelte';
154+
155+
if (BROWSER) {
156+
// bad! nothing can become interactive until this asynchronous work is done
157+
await hydratable('foo', get_slow_random_number);
158+
}
159+
</script>
160+
```
161+
143162
### hydration_attribute_changed
144163
145164
```

documentation/docs/98-reference/.generated/server-errors.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
22

3+
### async_local_storage_unavailable
4+
5+
```
6+
The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
7+
```
8+
9+
Some platforms require configuration flags to enable this API. Consult your platform's documentation.
10+
311
### await_invalid
412

513
```
@@ -14,10 +22,51 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render)
1422
The `html` property of server render results has been deprecated. Use `body` instead.
1523
```
1624

25+
### hydratable_clobbering
26+
27+
```
28+
Attempted to set `hydratable` with key `%key%` twice with different values.
29+
30+
%stack%
31+
```
32+
33+
This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can:
34+
- Ensure all invocations with the same key result in the same value
35+
- Update the keys to make both instances unique
36+
37+
```svelte
38+
<script>
39+
import { hydratable } from 'svelte';
40+
41+
// which one should "win" and be serialized in the rendered response?
42+
const one = hydratable('not-unique', () => 1);
43+
const two = hydratable('not-unique', () => 2);
44+
</script>
45+
```
46+
47+
### hydratable_serialization_failed
48+
49+
```
50+
Failed to serialize `hydratable` data for key `%key%`.
51+
52+
`hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
53+
54+
Cause:
55+
%stack%
56+
```
57+
1758
### lifecycle_function_unavailable
1859

1960
```
2061
`%name%(...)` is not available on the server
2162
```
2263

2364
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
65+
66+
### server_context_required
67+
68+
```
69+
Could not resolve `render` context.
70+
```
71+
72+
Certain functions such as `hydratable` cannot be invoked outside of a `render(...)` call, such as at the top level of a module.
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
2+
3+
### unresolved_hydratable
4+
5+
```
6+
A `hydratable` value with key `%key%` was created, but at least part of it was not used during the render.
7+
8+
The `hydratable` was initialized in:
9+
%stack%
10+
```
11+
12+
The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing
13+
the result inside a `svelte:boundary` with a `pending` snippet:
14+
15+
```svelte
16+
<script>
17+
import { hydratable } from 'svelte';
18+
import { getUser } from '$lib/get-user.js';
19+
20+
const user = hydratable('user', getUser);
21+
</script>
22+
23+
<svelte:boundary>
24+
<h1>{(await user).name}</h1>
25+
26+
{#snippet pending()}
27+
<div>Loading...</div>
28+
{/snippet}
29+
</svelte:boundary>
30+
```
31+
32+
Consider inlining the `hydratable` call inside the boundary so that it's not called on the server.
33+
34+
Note that this can also happen when a `hydratable` contains multiple promises and some but not all of them have been used.

documentation/docs/98-reference/.generated/shared-errors.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<!-- This file is generated by scripts/process-messages/index.js. Do not edit! -->
22

3+
### experimental_async_required
4+
5+
```
6+
Cannot use `%name%(...)` unless the `experimental.async` compiler option is `true`
7+
```
8+
39
### invalid_default_snippet
410

511
```

packages/svelte/messages/client-errors/errors.md

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,13 @@ $effect(() => {
100100

101101
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.
102102

103-
## experimental_async_fork
103+
## flush_sync_in_effect
104104

105-
> Cannot use `fork(...)` unless the `experimental.async` compiler option is `true`
105+
> Cannot use `flushSync` inside an effect
106+
107+
The `flushSync()` function can be used to flush any pending effects synchronously. It cannot be used if effects are currently being flushed — in other words, you can call it after a state change but _not_ inside an effect.
108+
109+
This restriction only applies when using the `experimental.async` option, which will be active by default in Svelte 6.
106110

107111
## fork_discarded
108112

@@ -116,6 +120,23 @@ Often when encountering this issue, the value in question shouldn't be state (fo
116120

117121
> `getAbortSignal()` can only be called inside an effect or derived
118122
123+
## hydratable_missing_but_required
124+
125+
> Expected to find a hydratable with key `%key%` during hydration, but did not.
126+
127+
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.
128+
129+
```svelte
130+
<script>
131+
import { hydratable } from 'svelte';
132+
133+
if (BROWSER) {
134+
// bad! nothing can become interactive until this asynchronous work is done
135+
await hydratable('foo', get_slow_random_number);
136+
}
137+
</script>
138+
```
139+
119140
## hydration_failed
120141

121142
> Failed to hydrate the application

packages/svelte/messages/client-warnings/warnings.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,23 @@ The easiest way to log a value as it changes over time is to use the [`$inspect`
124124
125125
> %handler% should be a function. Did you mean to %suggestion%?
126126
127+
## hydratable_missing_but_expected
128+
129+
> Expected to find a hydratable with key `%key%` during hydration, but did not.
130+
131+
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.
132+
133+
```svelte
134+
<script>
135+
import { hydratable } from 'svelte';
136+
137+
if (BROWSER) {
138+
// bad! nothing can become interactive until this asynchronous work is done
139+
await hydratable('foo', get_slow_random_number);
140+
}
141+
</script>
142+
```
143+
127144
## hydration_attribute_changed
128145
129146
> 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

packages/svelte/messages/server-errors/errors.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
## async_local_storage_unavailable
2+
3+
> The node API `AsyncLocalStorage` is not available, but is required to use async server rendering.
4+
5+
Some platforms require configuration flags to enable this API. Consult your platform's documentation.
6+
17
## await_invalid
28

39
> Encountered asynchronous work while rendering synchronously.
@@ -8,8 +14,43 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render)
814

915
> The `html` property of server render results has been deprecated. Use `body` instead.
1016
17+
## hydratable_clobbering
18+
19+
> Attempted to set `hydratable` with key `%key%` twice with different values.
20+
>
21+
> %stack%
22+
23+
This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can:
24+
- Ensure all invocations with the same key result in the same value
25+
- Update the keys to make both instances unique
26+
27+
```svelte
28+
<script>
29+
import { hydratable } from 'svelte';
30+
31+
// which one should "win" and be serialized in the rendered response?
32+
const one = hydratable('not-unique', () => 1);
33+
const two = hydratable('not-unique', () => 2);
34+
</script>
35+
```
36+
37+
## hydratable_serialization_failed
38+
39+
> Failed to serialize `hydratable` data for key `%key%`.
40+
>
41+
> `hydratable` can serialize anything [`uneval` from `devalue`](https://npmjs.com/package/uneval) can, plus Promises.
42+
>
43+
> Cause:
44+
> %stack%
45+
1146
## lifecycle_function_unavailable
1247

1348
> `%name%(...)` is not available on the server
1449
1550
Certain methods such as `mount` cannot be invoked while running in a server context. Avoid calling them eagerly, i.e. not during render.
51+
52+
## server_context_required
53+
54+
> Could not resolve `render` context.
55+
56+
Certain functions such as `hydratable` cannot be invoked outside of a `render(...)` call, such as at the top level of a module.

0 commit comments

Comments
 (0)