-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
feat: hydratable
#17154
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
base: main
Are you sure you want to change the base?
feat: hydratable
#17154
Changes from 15 commits
91882ee
352ebbe
2d475ac
2218b1e
d541688
fd92394
4b146b6
1ad5de0
a6b7bc2
f76c1aa
bc9df88
a025b9a
53ccd2e
37f2e0e
05e60b1
caad89b
5af28fe
923b086
9fed6f0
9343114
ce926c3
2ec34a2
90dd32b
3f3ad1a
640fabd
9af6013
9c88438
0c34711
74b878f
614fdf2
9884945
b5f2143
3c36c7e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'svelte': minor | ||
| --- | ||
|
|
||
| feat: `hydratable` API |
| 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 }); | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,10 +14,44 @@ You (or the framework you're using) called [`render(...)`](svelte-server#render) | |
| The `html` property of server render results has been deprecated. Use `body` instead. | ||
| ``` | ||
|
|
||
| ### hydratable_clobbering | ||
elliott-with-the-longest-name-on-github marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| ``` | ||
| Attempted to set hydratable with key `%key%` twice with different values. | ||
| First instance occurred at: | ||
| %stack% | ||
| Second instance occurred at: | ||
| %stack2% | ||
| ``` | ||
|
|
||
| This error occurs when using `hydratable` multiple times with the same key. To avoid this, you can: | ||
| - Ensure all invocations with the same key result in the same value | ||
| - Update the keys to make both instances unique | ||
|
|
||
| ```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. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we know, at the time we call
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is incorporated into the addendum -- I just couldn't dream up a way to be both helpful and put this much context into the addendum |
||
| ``` | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| <!-- This file is generated by scripts/process-messages/index.js. Do not edit! --> | ||
|
|
||
| ### unused_hydratable | ||
|
|
||
| ``` | ||
| A `hydratable` value with key `%key%` was created, but not used during the render. | ||
|
|
||
| Stack: | ||
| %stack% | ||
| ``` | ||
|
|
||
| The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing | ||
| the result inside a `svelte:boundary` with a `pending` snippet: | ||
|
|
||
| ```svelte | ||
| <script> | ||
| import { hydratable } from 'svelte'; | ||
| import { getUser } from '$lib/get-user.js'; | ||
|
|
||
| const user = hydratable('user', getUser); | ||
| </script> | ||
|
|
||
| <svelte:boundary> | ||
| <h1>{(await user).name}</h1> | ||
|
|
||
| {#snippet pending()} | ||
| <div>Loading...</div> | ||
| {/snippet} | ||
| </svelte:boundary> | ||
| ``` | ||
|
|
||
| Consider inlining the `hydratable` call inside the boundary so that it's not called on the server. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| ## unused_hydratable | ||
|
|
||
| > A `hydratable` value with key `%key%` was created, but not used during the render. | ||
| > | ||
| > Stack: | ||
| > %stack% | ||
| The most likely cause of this is creating a `hydratable` in the `script` block of your component and then `await`ing | ||
| the result inside a `svelte:boundary` with a `pending` snippet: | ||
|
|
||
| ```svelte | ||
| <script> | ||
| import { hydratable } from 'svelte'; | ||
| import { getUser } from '$lib/get-user.js'; | ||
| const user = hydratable('user', getUser); | ||
| </script> | ||
| <svelte:boundary> | ||
| <h1>{(await user).name}</h1> | ||
| {#snippet pending()} | ||
| <div>Loading...</div> | ||
| {/snippet} | ||
| </svelte:boundary> | ||
| ``` | ||
|
|
||
| Consider inlining the `hydratable` call inside the boundary so that it's not called on the server. |
Uh oh!
There was an error while loading. Please reload this page.