Skip to content

feat: hydratable and a more consistent remote functions model#15533

Merged
elliott-with-the-longest-name-on-github merged 41 commits into
mainfrom
elliott/remote-functions-hydratable-take-2
Mar 17, 2026
Merged

feat: hydratable and a more consistent remote functions model#15533
elliott-with-the-longest-name-on-github merged 41 commits into
mainfrom
elliott/remote-functions-hydratable-take-2

Conversation

@elliott-with-the-longest-name-on-github

@elliott-with-the-longest-name-on-github elliott-with-the-longest-name-on-github commented Mar 11, 2026

Copy link
Copy Markdown
Contributor

This PR makes a number of substantial changes to how remote queries work.

hydratable

Implementation-wise, it replaces our custom transport solution with hydratable for queries that are used during render -- this method of transport is more correct and prevents us from accidentally using stale data for queries in a small subset of cases. There's one breaking side effect of this: If you didn't render a query on the server, you can't render it during hydration.

It means this:

 <script>
  import { browser } from '$app/environment';
  const count = browser ? get_count() : null;
</script>

would throw during hydration because get_count tried to access cached data that did not exist. This is almost always an undesirable mistake: It means you introduced a waterfall on the client and blocked hydration. If you really do want this, you'd do something like this instead:

<script>
  import { onMount } from 'svelte';
  let count;
  onMount(() => {
    count = get_count();
  });
</script>

New rules around query usage

From now on, on the client, you can only access a query's data if that query is:

  1. created in a tracking context (i.e. at the top level of a script block, in a derived, in an effect, etc -- basically somewhere Svelte's reactivity system can "see" the query)
  2. the tracking context in which the query is created is still alive

It may be easier to illustrate when you can't create and use a query on the client:

  • universal load
  • event handlers
  • the top level of modules

We're making this change because otherwise it's basically impossible to reliably cache queries across your app. However, for most use cases, this probably doesn't cause any pain:

  • you can still use the methods that don't access query data anywhere: .refresh, .set, .withOverride are all available
  • if you need to get a query's data in a non-reactive context, you can make a one-off request to get that query's data with query().run(), which will return a plain old Promise resolving to your data

Bugfixes

  • queries are now wrapped in their own $effect.root when created. This prevents them from associating with their parent effect, making their usage more predictable when retrieved from the cache
  • .refresh is now a noop if there is no cached instance of the query, meaning you won't have a useless query request in some circumstances
  • caching / deduplicating should be more reliable, and it should be more obvious when you're trying to do something insane
  • there should now be no cases where stale cached data is used post-hydration (thank you, hydratable!)

@changeset-bot

changeset-bot Bot commented Mar 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 95fdd23

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@svelte-docs-bot

Copy link
Copy Markdown

Comment thread packages/kit/src/runtime/client/remote-functions/prerender.svelte.js Outdated
@elliott-with-the-longest-name-on-github elliott-with-the-longest-name-on-github marked this pull request as ready for review March 13, 2026 19:25
Comment thread packages/kit/src/runtime/client/remote-functions/query.svelte.js Outdated
…rowser consoles

This commit fixes the issue reported at packages/kit/src/runtime/client/remote-functions/query.svelte.js:58

**Bug Explanation:**

Three debug console.log statements were left in the production code at:

*   Line 58: `console.log(cache_key + ' bypassed hydratable');`
*   Line 292: `console.log(this._key, 'serving from the cache');`
*   Line 295: `console.log(this._key, 'made it past the cache');`

These statements appear to be development debugging aids that were not removed before committing. The codebase has a clear pattern for console.log usage:

1.  In build/CLI tools (acceptable since those run in Node.js during development)
2.  Guarded by `DEV` checks for runtime code (e.g., line 671 in respond.js uses `if (DEV && ...) { console.log(...) }`)

The unguarded console.log statements in query.svelte.js would execute on every query run/cache check in production, spamming end users' browser consoles with internal debugging messages like "query-key serving from the cache" and "query-key bypassed hydratable".

**Fix:**

Removed all three debug console.log statements since they serve no purpose for production users and were clearly left in accidentally (evidenced by commit messages like "i think it work" indicating development-in-progress code).


Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com>
Co-authored-by: elliott-with-the-longest-name-on-github <hello@ell.iott.dev>
Comment thread packages/kit/src/exports/public.d.ts Outdated
Comment thread packages/kit/src/runtime/app/server/remote/query.js Outdated
Comment thread packages/kit/src/runtime/client/remote-functions/shared.svelte.js
@elliott-with-the-longest-name-on-github elliott-with-the-longest-name-on-github merged commit 03670ad into main Mar 17, 2026
29 checks passed
@elliott-with-the-longest-name-on-github elliott-with-the-longest-name-on-github deleted the elliott/remote-functions-hydratable-take-2 branch March 17, 2026 20:19
@github-actions github-actions Bot mentioned this pull request Mar 17, 2026
elliott-with-the-longest-name-on-github pushed a commit that referenced this pull request Apr 3, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @sveltejs/kit@2.56.0

### Minor Changes

- breaking: rework client-driven refreshes
([#15562](#15562))


- breaking: stabilize remote function caching by sorting object keys
([#15570](#15570))


- breaking: add `run()` method to queries, disallow awaiting queries
outside render ([#15533](#15533))


- feat: support TypeScript 6.0
([#15595](#15595))


- breaking: isolate command-triggered query refresh failures per-query
([#15562](#15562))


- feat: use `hydratable` for remote function transport
([#15533](#15533))


- feat: allow `form` fields to specify a default value (`field.as(type,
value)`) ([#15577](#15577))

### Patch Changes

- fix: don't request new data when `.refresh` is called on a query with
no cache entry ([#15533](#15533))


- fix: allow using multiple remote functions within one async derived
([#15561](#15561))


- fix: avoid false-positive overridden Vite `base` setting warning when
setting a `paths.base` in `svelte.config.js`
([#15623](#15623))


- fix: manage queries in their own `$effect.root`
([#15533](#15533))


- fix: avoid `inlineDynamicImports` deprecation warning when building
the service worker with Vite 8
([#15550](#15550))


- fix: correctly escape backticks when precomputing CSS
([#15593](#15593))


- fix: discard obsolete forks before finishing navigation
([#15634](#15634))


- chore: tighten up override implementation
([#15562](#15562))


- fix: ensure the default Svelte 5 `error.svelte` file uses runes mode
([#15609](#15609))


- fix: deduplicate same-cache-key `batch` calls during SSR
([#15533](#15533))


- fix: decrement pending_count when form callback doesn't call submit()
([#15520](#15520))

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
ampelectrecuted pushed a commit to amp-mod/aw3 that referenced this pull request Apr 4, 2026
This PR contains the following updates:

| Package | Change | [Age](https://docs.renovatebot.com/merge-confidence/) | [Confidence](https://docs.renovatebot.com/merge-confidence/) |
|---|---|---|---|
| [@sveltejs/kit](https://svelte.dev) ([source](https://github.com/sveltejs/kit/tree/HEAD/packages/kit)) | [`2.55.0` → `2.56.1`](https://renovatebot.com/diffs/npm/@sveltejs%2fkit/2.55.0/2.56.1) | ![age](https://developer.mend.io/api/mc/badges/age/npm/@sveltejs%2fkit/2.56.1?slim=true) | ![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@sveltejs%2fkit/2.55.0/2.56.1?slim=true) |

---

### Release Notes

<details>
<summary>sveltejs/kit (@&#8203;sveltejs/kit)</summary>

### [`v2.56.1`](https://github.com/sveltejs/kit/blob/HEAD/packages/kit/CHANGELOG.md#2561)

[Compare Source](https://github.com/sveltejs/kit/compare/@sveltejs/kit@2.56.0...@sveltejs/kit@2.56.1)

##### Patch Changes

- chore: update JSDoc ([#&#8203;15640](sveltejs/kit#15640))

### [`v2.56.0`](https://github.com/sveltejs/kit/blob/HEAD/packages/kit/CHANGELOG.md#2560)

[Compare Source](https://github.com/sveltejs/kit/compare/@sveltejs/kit@2.55.0...@sveltejs/kit@2.56.0)

##### Minor Changes

- breaking: rework client-driven refreshes ([#&#8203;15562](sveltejs/kit#15562))

- breaking: stabilize remote function caching by sorting object keys ([#&#8203;15570](sveltejs/kit#15570))

- breaking: add `run()` method to queries, disallow awaiting queries outside render ([#&#8203;15533](sveltejs/kit#15533))

- feat: support TypeScript 6.0 ([#&#8203;15595](sveltejs/kit#15595))

- breaking: isolate command-triggered query refresh failures per-query ([#&#8203;15562](sveltejs/kit#15562))

- feat: use `hydratable` for remote function transport ([#&#8203;15533](sveltejs/kit#15533))

- feat: allow `form` fields to specify a default value (`field.as(type, value)`) ([#&#8203;15577](sveltejs/kit#15577))

##### Patch Changes

- fix: don't request new data when `.refresh` is called on a query with no cache entry ([#&#8203;15533](sveltejs/kit#15533))

- fix: allow using multiple remote functions within one async derived ([#&#8203;15561](sveltejs/kit#15561))

- fix: avoid false-positive overridden Vite `base` setting warning when setting a `paths.base` in `svelte.config.js` ([#&#8203;15623](sveltejs/kit#15623))

- fix: manage queries in their own `$effect.root` ([#&#8203;15533](sveltejs/kit#15533))

- fix: avoid `inlineDynamicImports` deprecation warning when building the service worker with Vite 8 ([#&#8203;15550](sveltejs/kit#15550))

- fix: correctly escape backticks when precomputing CSS ([#&#8203;15593](sveltejs/kit#15593))

- fix: discard obsolete forks before finishing navigation ([#&#8203;15634](sveltejs/kit#15634))

- chore: tighten up override implementation ([#&#8203;15562](sveltejs/kit#15562))

- fix: ensure the default Svelte 5 `error.svelte` file uses runes mode ([#&#8203;15609](sveltejs/kit#15609))

- fix: deduplicate same-cache-key `batch` calls during SSR ([#&#8203;15533](sveltejs/kit#15533))

- fix: decrement pending\_count when form callback doesn't call submit() ([#&#8203;15520](sveltejs/kit#15520))

</details>

---

### Configuration

📅 **Schedule**: (in timezone UTC)

- Branch creation
  - At any time (no schedule defined)
- Automerge
  - At any time (no schedule defined)

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4xMDQuMSIsInVwZGF0ZWRJblZlciI6IjQzLjEwNC4xIiwidGFyZ2V0QnJhbmNoIjoiZGV2ZWxvcCIsImxhYmVscyI6WyJLaW5kL0RlcGVuZGVuY2llcyJdfQ==-->

Reviewed-on: https://codeberg.org/ampmod/aw3/pulls/57
Co-authored-by: AmpMod Bot <ampmod-bot@noreply.codeberg.org>
Co-committed-by: AmpMod Bot <ampmod-bot@noreply.codeberg.org>
@AgarwalPragy

Copy link
Copy Markdown

if you need to get a query's data in a non-reactive context, you can make a one-off request to get that query's data with query().run(), which will return a plain old Promise resolving to your data

This isn't documented in https://svelte.dev/docs/kit/remote-functions

I was using a remote query inside a class method that gets invoked on a button click. After updating to the latest sveltekit, I need to add a .run() at the end of the query for it to work.

Rich-Harris added a commit to sveltejs/svelte that referenced this pull request Apr 9, 2026
)

Per
#17862 (comment),
this freezes the value of a derived if it was created inside a parent
effect that is now destroyed. This prevents the sort of bug where a
derived reads `foo.bar` even though `foo` is now `undefined`.

If the derived is dirty, a warning will be printed.

This PR also gets rid of some weirdness around `derived.parent` — it can
only ever be an `Effect | null`, and there's no need for
`get_derived_parent_effect`.

Blocked on sveltejs/kit#15533 and a follow-up
that switches remote functions to use `$effect.root` (since this
effectively undoes #17171), hence draft.

### Before submitting the PR, please make sure you do the following

- [x] It's really useful if your PR references an issue where it is
discussed ahead of time. In many cases, features are absent for a
reason. For large changes, please create an RFC:
https://github.com/sveltejs/rfcs
- [x] Prefix your PR title with `feat:`, `fix:`, `chore:`, or `docs:`.
- [x] This message body should clearly illustrate what problems it
solves.
- [x] Ideally, include a test that fails without this PR but passes with
it.
- [x] If this PR changes code within `packages/svelte/src`, add a
changeset (`npx changeset`).

### Tests and linting

- [x] Run the tests with `pnpm test` and lint the project with `pnpm
lint`

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Simon H <5968653+dummdidumm@users.noreply.github.com>
@ConProgramming

Copy link
Copy Markdown
Contributor

Hi, this has broken some existing integration with Tanstack Query v6 that we relied on. Is there a recommended pattern to keep this functional with Tanstack Query?

Example in +page.svelte.

<script lang="ts">
    import { createQuery } from "@tanstack/svelte-query";
    import { query_program } from "../../programs/(remote)/programs.remote.ts";
    
    const program_query = createQuery(() => ({
        queryKey: ["program", programId] as const,
        queryFn: async () => {
            return await query_program(programId);
        },
    }));
 </script>

@sergiocampama

Copy link
Copy Markdown

This also broke the type system, commands returning Promise<string> are not reporting to be Promise<Promise<string>> on client code

@elliott-with-the-longest-name-on-github

Copy link
Copy Markdown
Contributor Author

@sergiocampama / @ConProgramming please file issues for these, happy to discuss there -- not going to spam other maintainers who were following this PR with notifications

elliott-with-the-longest-name-on-github pushed a commit that referenced this pull request Apr 17, 2026
…-reactive contexts (#15699)

As of #15533, `query().current` no longer works in non-reactive
contexts. I believe this is unwanted behavior -- take the following
scenario:

```svelte
<script>
  import { list_todos } from '$lib/todos.remote';
  $effect(() => {
    const events = subscribe_to_something_external();
    events.on('new-todo', (item) => {
      // I don't want to call `.run()` here, because if `list_todos`
      // is not currently used, I don't care about the result
      // (it would just waste a network request)
      const current = list_todos().current;
      if (!current) return;
      list_todos().set([data.item, ...current]);
    });

  });
</script>
```

This PR fixes it by simply restoring the old behavior of `.current`
returning `undefined` if the query has not been called yet.
@AgarwalPragy

AgarwalPragy commented May 26, 2026

Copy link
Copy Markdown

For anyone coming back to this - .run() is now gone - removed in #15779

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants