Skip to content

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

Open
elliott-with-the-longest-name-on-github wants to merge 10 commits intomainfrom
elliott/remote-functions-hydratable-take-2
Open

feat: hydratable and a more consistent remote functions model#15533
elliott-with-the-longest-name-on-github wants to merge 10 commits intomainfrom
elliott/remote-functions-hydratable-take-2

Conversation

@elliott-with-the-longest-name-on-github
Copy link
Contributor

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

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
Copy link

changeset-bot bot commented Mar 11, 2026

⚠️ No Changeset found

Latest commit: 72bc665

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

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

@svelte-docs-bot
Copy link

@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
…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 on lines +2142 to +2143
* Unlike awaiting the resource directly, this can be used anywhere
* (load functions, event handlers, etc) without requiring a reactive context.
Copy link
Member

Choose a reason for hiding this comment

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

we'd talked about making it not work in effects — is that the plan? (or already implemented, though i'll figure that out by carrying on reading) if so this comment will need to change

Choose a reason for hiding this comment

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

not implemented. though I'm still ambivalent. I don't think it would be too hard to implement and it might be a good idea.

Comment on lines +85 to +86
start: () => get_response(__, arg, state, get_remote_function_result),
refresh: () => get_remote_function_result()
Copy link
Member

Choose a reason for hiding this comment

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

we can simplify this signature

Suggested change
start: () => get_response(__, arg, state, get_remote_function_result),
refresh: () => get_remote_function_result()
get_remote_function_result

'x-sveltekit-search': page.url.search
}));
return untrack(() => {
const url = navigating.current?.to?.url ?? page.url;
Copy link
Member

Choose a reason for hiding this comment

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

this feels a little brittle — you could navigate, triggering a load function that will eventually call a query, then navigate again before it actually happens. not yet sure what the right way to think about it is

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.

2 participants