Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

The RFC was tweaked, see #13897 (comment) for a list of changes

tl;dr

Remote functions are a new concept in SvelteKit that allow you to declare functions inside a .remote.ts file, import them inside Svelte components and call them like regular functions. On the server they work like regular functions (and can access environment variables and database clients and so on), while on the client they become wrappers around fetch. If you're familiar with RPC and 'server functions', this is basically our take on the concept, that addresses some of the drawbacks we've encountered with other implementations of the idea.

You can try it out soon by installing from the corresponding PR once it's out...

... and add the experimental options

const config = {
	// ...

	kit: {
		// add this:
		experimental: {
			remoteFunctions: true
		},
		// ...
	},

	// we recommend using this in combination with Svelte's new async capabilities
	compilerOptions: {
		experimental: { async: true }
	}
};

We'll also link an example here, soon.

Background

Today, SvelteKit's data loading is based on the concept of loaders. You declare a load function inside a +page/layout(.server).ts file, fetch the required data for the whole page in it, and retrieve the result via the data prop inside the sibling +page/layout.svelte file.

This allows for a very structured approach to data loading and works well for sites where the loaded data is used on the whole page. When that isn't so clear-cut, some drawbacks become apparent:

  • it leads to implicit coupling between what seem like disconnected files:
    • colocation suffers
    • data only needed in one corner of the page, or only under certain conditions, still needs to be put into the loader
    • deleting or refactoring code becomes harder since you need to keep in mind both where stuff is loaded and where it’s used
  • sharing data means moving its data loading up and down the layout loader tree, leading to more complexity
  • refetching data happens on a loader level, if you want it to be more granular SvelteKit can't help you

Additionally, since load and the resulting data prop are somewhat disconnected, we have to resort to very clever but somewhat weird solutions like generating hidden types you import as ./$types or even using a TypeScript plugin to avoid having to import the types yourself. An approach where we can use TypeScript natively would simplify all this and make it more robust.

Lastly, apart from form actions SvelteKit doesn't give you a good way to mutate data. You can use +server.ts files and do fetch requests against these endpoints, but it's a lot of ceremony and you lose type safety.

Asynchronous Svelte

A couple of weeks ago we introduced Asynchronous Svelte, a proposal to allow using await at the top level of Svelte components and inside the template.

This in itself is already valuable, but the way SvelteKit's data loading is architected right now you can't take full advantage of it inside SvelteKit.

Requirements

A solution should fix these drawbacks and take advantage of Svelte's capabilities, and specifically should:

  • be secure and intuitive
  • increase colocation, moving data loading closer to where it's used
  • make loading and mutation type-safe
  • put control of granularity in the developer's hands
  • fully take advantage of asynchronous Svelte
  • be maximally efficient

An important additional requirement is that modules that can run in the client (including components) must never include code that can only run on the server. A remote function must be able to safely access things like database clients and environment variables that should not (or cannot) be accessed from the client.

In practice, this means that remote functions must be declared in a separate module. Over the last few years various systems have experimented with 'server functions' declared alongside universal/client code, and we're relieved to see a growing consensus that this is a flawed approach that trades security and clarity for a modicum of convenience. You're one innocent mistake away from leaking sensitive information (such as API keys or the shape of your database), and even if tooling successfully treeshakes it away, it may remain in sourcemaps. While no framework can completely prevent you from spilling secrets, we think colocating server and client code makes it much more likely.

Allowing server functions to be declared in arbitrary locations also masks the fact that they are effectively creating a publicly accessible endpoint. Even in systems that prevent server functions from being declared in client code (such as "use server" in React Server Components), experienced developers can be caught out. We prefer a design that emphasises the public nature of remote functions rather than the fact that they run on the server, and avoids any confusion around lexical scope.

Design

Before we jump in we want to make clear that this does not affect load functions, they continue to work as is.

Remote functions are declared inside a .remote.ts file. You can import them inside Svelte components and call them like regular async functions. On the server you import them directly; on the client, the module is transformed into a collection of functions that request data from the server.

Today we’re introducing four types of remote function: query, form, command and prerender.

query

Queries are for reading dynamic data from the server. They can have zero or one arguments. If they have an argument, you're encouraged to validate the input via a schema which you can create with libraries like Zod (more details in the upcoming Validation section). The argument is serialized with devalue, which handles types like Date and Map in addition to JSON, and takes the transport hook into account.

import z from 'zod';
import { query } from '$app/server';
import * as db from '$lib/server/db';

export const getLikes = query(z.string(), async (id) => {
  const [row] = await db.sql`select likes from item where id = ${id}`;
  return row.likes;
});

When called during server-rendering, the result is serialized into the HTML payload so that the data isn't requested again during hydration.

<script>
  import { getLikes } from './data.remote';
  
  let { item } = $props();
</script>

<p>likes: {await getLikes(item.id)}</p>

Async SSR isn’t yet implemented in Svelte, which means this will only load in the client for now. Once SSR is supported, this will be able to hydrate correctly, not refetching data

Queries are thenable, meaning they can be awaited. But they're not just promises, they also provide properties like loading and current (which contains the most recent value, but is initially undefined) and methods like override(...) (see the section on optimistic UI, below) and refresh(), which fetches new data from the server. We’ll see an example of that in a moment.

Query objects are cached in memory for as long as they are actively used, using the serialized arguments as a key — in other words myQuery(id) === myQuery(id). Refreshing or overriding a query will update every occurrence of it on the page. We use Svelte's reactivity system to intelligently clear the cache to avoid memory leaks.

form

Forms are the preferred way to write data to the server:

import z from 'zod';
import { query, form } from '$app/server';
import * as db from '$lib/server/db';

export const getLikes = query(z.string(), async (id) => {/*...*/});

export const addLike = form(async (data: FormData) => {
  const id = data.get('id') as string;

  await sql`
    update item
    set likes = likes + 1
    where id = ${id}
  `;

  // we can return arbitrary data from a form function
  return { success: true };
});

A form object such as addLike has enumerable properties — method, action and onsubmit — that can be spread onto a <form> element. This allows the form to work without JavaScript (i.e. it submits data and reloads the page), but it will also automatically progressively enhance the form, submitting data without reloading the entire page.

<script>
  import { getLikes, addLike } from './data.remote';
  
  let { item } = $props();
</script>

<form {...addLike}>
  <input type="hidden" name="id" value={item.id} />
  <button>add like</button>
</form>

<p>likes: {await getLikes(item.id)}</p>

By default, all queries used on the page (along with any load functions) are automatically refreshed following a form submission, meaning getLikes(...) will show updated data.

In addition to the enumerable properties, addLike has non-enumerable properties such as result, containing the return value, and enhance which allows us to customize how the form is progressively enhanced. We can use this to indicate that only getLikes(...) should be refreshed and through that also enable single-flight mutations — meaning that the updated data for getLikes(...) is sent back from the server along with the form result. Additionally we provide nicer behaviour in the case that the submission fails (by default, an error page will be shown):

<script>
  import { getLikes, addLike } from './data.remote';
  
  let { item } = $props();
</script>

{#if addLike.result?.success}
  <p>success!</p>
{/if}

<form {...addLike.enhance(async ({ submit }) => {
  try {
    // by passing queries to `.updates(...)` we will prevent a global refresh and the
    // refreshed data is sent together with the submission response (single flight mutation)
    await submit().updates(getLikes(item.id));
  } catch (error) {
    // instead of showing an error page,
    // present a demure notification
    showToast(error.message);
  }
}}>
  <input type="hidden" name="id" value={item.id} />
  <button>add like</button>
</form>

<p>likes: {await getLikes(item.id)}</p>

form.result need not indicate success — it can also contain validation errors along with any data that should repopulate the form on page reload, much as happens today with form actions.

Alternatively we can also enable single-flight mutations by adding the refresh call to the server, which means all calls to addLike will leverage single-flight mutations compared to only those who use submit.updates(...):

import { query, form } from '$app/server';
import * as db from '$lib/server/db';

export const getLikes = query(async (id: string) => {
  const [row] = await sql`select likes from item where id = ${id}`;
  return row.likes;
});

export const addLike = form(async (data: FormData) => {
  const id = data.get('id') as string;

  await sql`
    update item
    set likes = likes + 1
    where id = ${id}
  `;
  
+ await getLikes(id).refresh();

  // we can return arbitrary data from a form function
  return { success: true };
});

command

For cases where serving no-JS users is impractical or undesirable, command offers an alternative way to write data to the server.

import z from 'zod';
-import { query, form } from '$app/server';
+import { query, command } from '$app/server';
import * as db from '$lib/server/db';

export const getLikes = query(z.string(), async (id) => {
  const [row] = await sql`select likes from item where id = ${id}`;
  return row.likes;
});

-export const addLike = form(async (data: FormData) => {
-  const id = data.get('id') as string;
+export const addLike = command(z.string(), async (id) => {

  await sql`
    update item
    set likes = likes + 1
    where id = ${id}
  `;
  
  getLikes(id).refresh();

  // we can return arbitrary data from a command
  return { success: true };
});

This time, simply call addLike, from (for example) an event handler:

<script>
  import { getLikes, addLike } from './data.remote';
  
  let { item } = $props();
</script>

<button
  onclick={async () => {
    try {
      await addLike();
    } catch (error) {
      showToast(error.message);
    }
  }}
>
  add like
</button>

<p>likes: {await getLikes(item.id)}</p>

Commands cannot be called during render.

As with forms, we can refresh associated queries on the server during the command or via .updates(...) on the client for a single-flight mutation, otherwise all queries will automatically be refreshed.

prerender

This function is like query except that it will be invoked at build time to prerender the result. Use this for data that changes at most once per redeployment.

import z from 'zod';
import { prerender } from '$app/server';

export const getBlogPost = prerender(z.string(), (slug) => {
  // ...
});

You can use prerender functions on pages that are otherwise dynamic, allowing for partial prerendering of your data. This results in very fast navigation, since prerendered data can live on a CDN along with your other static assets.

When the entire page has export const prerender = true, you cannot use queries, as they are dynamic.

Prerendering is automatic, driven by SvelteKit's crawler, but you can also provide an entries option to control what gets prerendered, in case some pages cannot be reached by the crawler:

import z from 'zod';
import { prerender } from '$app/server';

export const getBlogPost = prerender(
  z.string(),
  (slug) => {
    // ...
  },
  {
    entries: () => ['first-post', 'second-post', 'third-post']
  }
);

If the function is called at runtime with arguments that were not prerendered it will error by default, as the code will not have been included in the server bundle. You can set dynamic: true to change this behaviour:

import z from 'zod';
import { prerender } from '$app/server';

export const getBlogPost = prerender(
  z.string(),
  (slug) => {
    // ...
  },
  {
+   dynamic: true,
    entries: () => ['first-post', 'second-post', 'third-post']
  }
);

Optimistic updates

Queries have an withOverride method, which is useful for optimistic updates. It receives a function that transforms the query, and must be passed to submit().updates(...) or myCommand.updates(...):

<script>
  import { getLikes, addLike } from './data.remote';
  
  let { item } = $props();
</script>

<button
  onclick={async () => {
    try {
-      await addLike();
+      await addLike().updates(getLikes(item.id).withOverride((n) => n + 1));
    } catch (error) {
      showToast(error.message);
    }
  }}
>
  add like
</button>

<p>likes: {await getLikes(item.id)}</p>

You can of course do const likes = $derived(getLikes(item.id)) in your <script> and then do likes.withOverride(...) and {await likes} if you prefer, but since getLikes(item.id) returns the same object in both cases, this is optional

Multiple overrides can be applied simultaneously — if you click the button multiple times, the number of likes will increment accordingly. If addLike() fails, the override releases and will decrement it again, otherwise the updated data (sans override) will match the optimistic update.

Validation

Data validation is an important part of remote functions. They look like regular JavaScript functions but they are actually auto-generated public endpoints. For that reason we strongly encourage you to validate the input using a Standard Schema object, which you create for example through Zod:

import { query } from '$app/server';
import { z } from 'zod';

const schema = z.object({
  id: z.string()
});

export const getStuff = query(schema, async ({ id }) => {
    // `id` is typed correctly. if the function
    // was called with bad arguments, it will
    // result in a 400 Bad Request response
});

By default a failed schema validation will result in a generic 400 response with just the text Bad Request. You can adjust the returned shape by implementing the handleValidationError hook in hooks.server.js. The returned shape must adhere to the shape of App.Error.

// src/hooks.server.ts
import z from 'zod';

export function handleValidationError({ result }) {
  return { validationErrors: z.treeifyError(result.error)}
}

If you wish to opt out of validation (for example because you validate through other means, or just know this isn't a problem), you can do so by passing 'unchecked' as the first argument instead:

import { query } from '$app/server';

export const getStuff = query('unchecked', async ({ id }: { id: string }) => {
    // the shape might not actually be what TypeScript thinks since bad actors might call this function with other arguments
});

In case your query does not accept arguments you don't need to pass a schema or 'unchecked' - validation is added under the hood on your behalf to check that no arguments are passed to this function:

import { query } from '$app/server';

export const getStuff = query(() => {
    // ...
});

The same applies to prerender and command. form does not accept a schema since you are always passed a FormData object which you need to parse and validate yourself.

Accessing the current request event

SvelteKit exposes a function called getRequestEvent which allows you to get details of the current request inside hooks, load, actions, server endpoints, and the functions they call.

This function can now also be used in query, form and command, allowing us to do things like reading and writing cookies:

import { getRequestEvent, query } from '$app/server';
import { findUser } from '$lib/server/db';

export const getProfile = query(async () => {
  const user = await getUser();
  
  return {
    name: user.name,
    avatar: user.avatar
  };
});

// this function could be called from multiple places
function getUser() {
  const { cookies, locals } = getRequestEvent();
  
  locals.userPromise ??= findUser(cookies.get('session_id'));
  return await locals.userPromise;
}

Note that some properties of RequestEvent are different in remote functions. There are no params or route.id, and you cannot set headers (other than writing cookies, and then only inside form and command functions), and url.pathname is always / (since the path that’s actually being requested by the client is purely an implementation detail).

Redirects

Inside query, form and prerender functions it is possible to use the redirect(...) function. It is not possible inside command functions, as you should avoid redirecting here. (If you absolutely have to, you can return a { redirect: location } object and deal with it in the client.)

Future work / open questions

Server caching

We want to provide some kind of caching mechanism down the line, which would give you the speed of prerendering data while also reacting to changes dynamically. If you're using Vercel, think of it as ISR on a function level.

We would love to hear your opinions on this matter and gather feedback around the other functions before committing to a solution.

Client caching

Right now a query is cached and deduplicated as long as there's one active subscription to it. Maybe you want to keep things around in memory a little longer, for example to make back/forward navigation instantaneous? We haven't explored this yet (but have some ideas) and would love to hear your use cases (or lack thereof) around this.

Prerendered data could be kept in memory as long as the page is open — since we know it’s unchanging, it never needs to be refetched. The downside is that the memory could then never be freed up. Perhaps this needs to be configurable.

Conversely, for queries that we know will become stale after a certain period of time, it would be useful if the query function could communicate to the client that it should be refetched after n seconds.

Batching

We intend to add client-side batching (so that data from multiple queries is fetched in a single HTTP request) and server-side batching that solves the n + 1 problem, though this is not yet implemented.

Streaming

For real-time applications, we have a sketch of a primitive for streaming data from the server. We’d love to hear your use cases.

You must be logged in to vote

Replies: 178 comments · 693 replies

Comment options

yet another long-awaited feature

You must be logged in to vote
1 reply
@Ayushkumar48
Comment options

but It will definitely make SvelteKit a great framework like a Swiss knife of the frameworks.

Comment options

The hype is real for this - amazing changes - and validation!!! Omg..

For real-time applications, we have a sketch of a primitive for streaming data from the server. We’d love to hear your use cases.

*cough* Zero *cough* 👀

You must be logged in to vote
7 replies
@saturnonearth
Comment options

I assume it refers to https://zero.rocicorp.dev...

Ha yes!! Probably should have been more clear. I am just a strong advocate for sync/local-first and would love Svelte to lead the charge.

Loving the future for Svelte and SvelteKit

@JonathonRP
Comment options

Possibly also convex 😉

@samal-rasmussen
Comment options

All these real time frameworks are way overbuilt. All we need is a primitive like the command fn that subscribes to data over a websocket and returns a signal/observable/store, and we can build our own solutions on top of that.

@Rich-Harris
Comment options

This should give you an idea where our head's at: #12973 (comment)

@garth
Comment options

FWIW my use case for steaming is primarily Yjs updates. Since SSE is limited to text (due to the text/event-stream mime type), binary data would probably require base64ing which would be an unacceptable overhead. Ideally we would need a transport that can efficiently handle binary (eg Uint8Array).

Comment options

I am about to cry 🥹 This is a freaking great feature!.

You must be logged in to vote
0 replies
Comment options

Brilliant 🎉

You must be logged in to vote
0 replies
Comment options

I'm excited to see the idea for validation! Have you considered something like this to avoid the extra import and slightly awkward nested functions?

export const getStuff = query.validate(schema, async ({ id }) => {
  // `id` is typed correctly. if the function
  // was called with bad arguments, it will
  // result in a 422 response
});
You must be logged in to vote
5 replies
@Rich-Harris
Comment options

Yes, considered and rejected — it's more surface area (you need command.validate, form.validate, prerender.validate and so on) and less composable (you might want to use validation with a function that's called by a query, rather than the query itself). Much better to use composition

@ottomated
Comment options

Makes sense! My only counter-argument would be that validation should be the default case 99.9% of the time, and it doesn't feel like that with the composed approach. I don't know if it exists, but I would love to see a clean solution that pushes users towards validating all their functions (I hope we agree that all server functions being validated is the ideal!)

@Rich-Harris
Comment options

I'm going to copy and paste what I wrote during an internal discussion on this topic:

In the case of a query, which has no side-effects (unless you are actively choosing to shoot yourself in the foot), malformed input will just return something useless — there's no way to use this for mischief:

export const find_nearest_city = query((lat: number, lng: number) => {
  let min_distance = Infinity;
  let closest = null;

  for (const city of cities) {
    const distance = get_distance(city, { lat, lng });
    if (distance < min_distance) {
      min_distance = distance;
      closest = city;
    }
  }

  return city;
});

Even with commands, sometimes there are no arguments — there's nothing to validate here, so if command expected a schema it would just be annoying boilerplate:

export const hit_counter = command(() => {
  await db.increment('hits');
});

And even when there are arguments, the value of validation is overstated. In a case like this, I only need to check that the user is authenticated (which Standard Schema obviously can't help me with). Validating that id is a number doesn't really give me any additional protection, because the requester can only interact with their own posts, and because if they passed something other than a number the most likely outcome is that db.get_post(id) would just fail.

// we need validation here, but of the user, not the arguments,
// and the logic really needs to go inside the function itself
export async function delete_post(id: number) {
  const { locals } = getRequestEvent();

  if (!locals.user) error(401);

  const post = await db.get_post(id);
  if (post.author !== locals.user.id) error(403);

  await db.delete_post(id);
}

Validation doesn't magically make things secure. It's a valid user ID? Cool. Whose?

So I don't think we should require validation, we should just encourage it by offering a validate function that uses Standard Schema:

import { validate } from '@sveltejs/kit';
import * as v from 'valibot';

const schema = v.tuple([
  v.pipe(v.number(), v.minValue(-180), v.maxValue(180)),
  v.pipe(v.number(), v.minValue(-180), v.maxValue(180))
]);

export const find_nearest_city = query(
  validate(schema, ([lat, lng]) => {
    // ...
  })
);

Given that a validation error could occur because of someone probing remote function endpoints, the appropriate response here is a 400 with a generic 'Bad Request' message, rather than detailed instructions on how to pass validation. More granular checks like 'this email address is already in use' should be handled manually by the developer and result in 422s.

@ottomated
Comment options

Here's an example of a vulnerability that could arise:

export const delete_post = command((id: number) => {
  const post = await posts.findOne({
    id
  });
  if (!user_can_delete(post)) error(403);
  await posts.delete({
    id
  });
});

Looks fine, right? What if a malicious user sends the following:

await delete_post({ lte: 20 });

The findOne could return post 20 and pass the check, and then delete all posts from 0-20.

@jose-manuel-silva
Comment options

@Rich-Harris I get the reasoning about validation not being a magic security bullet, and the composability stuff makes sense.

But @ottomated's injection example shows how dangerous the "malformed input just returns useless data" assumption can be. The TypeScript signature says id: number but that's meaningless at runtime - malicious clients can send whatever JSON they want.

Looking at how other libraries do this, the nested approach feels weird:

tRPC does:

.input(schema)
.mutation(({ input }) => { ... })

next-safe-action does:

action(schema, (input) => { ... })

Everyone else puts validation first, handler second. SvelteKit's approach of burying the handler inside the validation function is pretty unique.

I get your composability point about validating sub-functions, but you could still achieve that with a validation-first API:

// Common case: validation prominent  
export const deleteUser = command(
  z.object({ id: z.string() }),  
  async ({ id }) => {
    // id is typed correctly, 400 on bad input
  }
);

// Your composability case: validate a sub-function
const validateInput = z.object({ id: z.string() }).parse;

export const complexThing = command(
  validateInput,  // any transformation function works
  async (validated) => { 
    // can call other functions with validateInput
  }
);

// Edge case: explicit opt-out
export const hitCounter = command.raw(() => {
  await db.increment('hits');
});

This keeps your composability while making validation visually prominent for the dangerous cases. The current nested approach makes it too easy to skip validation.

What do you think?

Comment options

wow!

You must be logged in to vote
0 replies
Comment options

Wow, this is awesome! It seems so intuitive and satisfying to use!

You must be logged in to vote
0 replies
Comment options

sounds great!

I have been struggling to implement a fairly complex SvelteKit-app that renders sequences of sensor data from lidars, cameras and radars.

each frame in a sequence can be 20-100 mb. at the same time, the sensor data is pretty static so caching can and will be used on as many levels as possible.

super interested in what you have planned for caching!

Server caching

We want to provide some kind of caching mechanism down the line, which would give you the speed of prerendering data while also reacting to changes dynamically.

this would be great and it would basically replace what I have implemented on my own to cache requests. it would be great with control over at least max size when used as some LRU-cache. but also max size in terms of size on the disk and maybe also some TTL. and of course some way to manually invalidate the cache per function or globally, if needed.

Client caching

Right now a query is cached and deduplicated as long as there's one active subscription to it. Maybe you want to keep things around in memory a little longer, for example to make back/forward navigation instantaneous? We haven't explored this yet (but have some ideas) and would love to hear your use cases (or lack thereof) around this.

Prerendered data could be kept in memory as long as the page is open — since we know it’s unchanging, it never needs to be refetched. The downside is that the memory could then never be freed up. Perhaps this needs to be configurable.

yeah this would also be useful! I would even go one step further and consider some sort of persistent cache for users - maybe in IndexedDB or something along those lines?

otherwise that is something I plan to implement anyways - so I don’t have to stream hundreds of megabytes to a user if they accidentally refresh the page. again one thing less I would have to implement in userland so if this was provided as some opt-in feature by sveltekit it would be the dream.

the same level of configuration I brought up for the server caching would be useful in a persistent client side cache as well

—-

please let me know if I can provide feedback in any more structured way, I would love to test this out in practice later on and help out as much as I can

You must be logged in to vote
1 reply
@darrylyeo
Comment options

A built-in client-side cache would be amazing. Would allow me to completely remove TanStack Query as a dependency.

Comment options

This reminds me of the days I was a GWT expert, back in 2010-16. I must say there were disappointments with the RPC calls architecture, but I forgot the use cases. What I remember is that it was frustrating to call them manually, eg. from a mobile app, so I had to isolate then as only callers to service methods (having them like boilerplate code). Maybe the architecture would allow a non proprietary protocol so that it can be implemented in case of need...

You must be logged in to vote
0 replies
Comment options

Where can I subscribe so that I get a notification once this is testable? The form part is incredibly valuable <3

You must be logged in to vote
3 replies
@dummdidumm
Comment options

dummdidumm Jun 17, 2025
Maintainer Author

PR will hopefully go up later this week, we'll update the discussion with a link to it and example then.

@Defman
Comment options

Any updates =)

@rebasecase
Comment options

#13957

Comment options

This is going to be incredible, this and async address all of the features I have been wanting from Svelte.

One question for the team, have you considered adding a Form component property instead of spreading? I feel like could be a bit more readable, especially with enhance.

<script>
  import { getLikes, addLike } from './data.remote';
  
  let { item } = $props();
</script>

<addLike.Form enhance={async () => {...}}>
  <input type="hidden" name="id" value={item.id} />
  <button>add like</button>
</addLike.Form>

<p>likes: {await getLikes(item.id)}</p>
You must be logged in to vote
3 replies
@Rich-Harris
Comment options

Aesthetically I'd prefer <addLike.form ...> (and <addLike.button> for <button formAction="...">), but yes, this is worth considering.

The main reason not to do it is that you can no longer use element-specific directives like transitions — our goal is to replace those with more flexible programmatic APIs that can be used with attachments, but for now attachments only replace actions, and there's still stuff like bind:clientWidth which works on elements but not components.

@theetrain
Comment options

I'm not sure if <addLike.Form> (or <addLike.form>) resemble any existing markup pattern; I believe custom elements can only be dash-separated like <addlike-form>; and Svelte components must be capitalized like <AddLike.Form>. Even when explained, it's challenging for me to mentally map <addLike.Form> to an eventually-rendered <form>.

How about this twist:

<script>
  import { getLikes, addLike } from './data.remote';

+ import { Form } = addLike
  
  let { item } = $props();
</script>

+<Form enhance={async () => {...}}>
  <input type="hidden" name="id" value={item.id} />
  <button>add like</button>
+</Form>

<p>likes: {await getLikes(item.id)}</p>

Though I'm unsure if we can compiler away the theoretical Form component property on addLike when unused.

I still prefer the markup as proposed, with <form {...addLike}> and <form {...addLike.enhance(() => {})}> since it fits neatly in the world of Svelte and HTML. There's also the concern of styling <Form> since there's no way currently to pass scoped styles down to components.

@Rich-Harris
Comment options

Svelte components must be capitalized

nope, they must be capitalized or use dot notation

Comment options

if await getLikes(item.id) is like react/solid <Suspense> how do we have a fallback?

You must be logged in to vote
1 reply
@Rich-Harris
Comment options

with <svelte:boundary> sveltejs/svelte#15845

Comment options

EDIT: Ignore this. I didn't see it was a template string.

db.sqlselect likes from item where id = ${id}

Please use prepared statements in your sql examples 😭

You must be logged in to vote
5 replies
@Rich-Harris
Comment options

Why?

@samal-rasmussen
Comment options

Because teaching newbies to trust that a function that accepts and runs an sql query against a db will be doing proper sanitizing for them, will let them down.

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

not really, this is just how modern sql libraries do parameterization: https://orm.drizzle.team/docs/sql#sql-template, https://www.npmjs.com/package/@vercel/postgres, https://www.npmjs.com/package/postgres

@JabSYsEmb
Comment options

@samal-rasmussen
Comment options

Lord I didn't even spot it was a template literal. Sorry about that. That is very different from a function call. Modern js is magical.

(Sometimes I wonder how I manage to be a professional developer even when I am often character blind like this.)

Comment options

While I don't think I will use this many times since I build mostly true SPAs with a separate backend, I really believe these will be very useful for fullstack apps or backend-for-frontend apps.

There are two additions I would add:

Output validation

Not so much due to the validation but due to the validator being able to transform the output (setting default values for null/undefined fields, removing extra keys, etc.).

I think a tRPC-like signature like this would be more ergonomic if output validation is added but the signature is definitely not a big deal:

const my_procedure = rpc
    .input(input_schema)
    .output(output_schema)
    .query(schema, async (input) => {
        //body
      })
    //maybe allow chaining a .mutation here to create a mutation that implicitly invalidates the query?

Support for middleware (using the tRPC-like signature above)

Middlewares are a great way to manage dependency injection in a declarative manner.

const my_procedure = rpc
    .input(input_schema)
    .output(output_schema)
    .middleware(services) //Whatever is returned from a middleware is passed to the next middleware as the first parameter
    .middleware(logging)
    .middleware(stats)
    .middleware(with_cache({storage: 'redis', ttl: 3600})) //The function returned by with_cache() would be accessing a redis service returned by the first middleware
    .query(schema, async (input, ctx) => { //ctx holds the awaited return value of the last middleware
        //body
      })

I also would like to know what are the plans regarding load functions.
Is this considered a replacement that will eventually lead to load functions being marked as deprecated?

Personally I think load functions fill a different role, specially on SPAs, where they can be used as a mechanism for dependency/service injection and state management. I even consider them a better context alternative.
When I started building SPAs with svelte-kit I overlooked them because they were way more limited as they couldn't use browser APIs and did not support returning $state-ful values, but now I think they are the best cross-component state and dependency management solutions out there for SPAs: state and services stay scoped to the page/layout where they are defined (you can even create +layout.ts files without their corresponding +layout.svelte file just for state/dependency management purposes!), automatic and intuitive composition due to their monadic nature, automatic invalidation, type-safety, automatic injection via the data prop, ability to preload state and initialize services needed by the page you will visit next... The only thing I miss is not being able to define teardown logic in load functions to deinit services, but it is a minor inconvenience given the DX they provide.

You must be logged in to vote
0 replies
Comment options

output validation

Why would this ever be needed / isn't this what TypeScript is for? You're already in control of the data you return from the function, so just... don't return data in the wrong shape / type the result of your function. (Am I missing something?)

middleware

Is this necessary in a world where you have access to getRequestEvent? Wouldn't you put your services on locals and access them there?

You must be logged in to vote
4 replies
@Rich-Harris
Comment options

@elliott-with-the-longest-name-on-github i assume you meant this to be a reply to the previous thread

@kran6a
Comment options

Why would this ever be needed / isn't this what TypeScript is for? You're already in control of the data you return from the function, so just... don't return data in the wrong shape / type the result of your function. (Am I missing something?)

This requires using a return type annotation on the handler function. Otherwise, a query like "SELECT * FROM table" or the ORM equivalent would return new fields should you update your DB schema without breaking anything in typescript.

Having to annotate return types increase maintenance/refactoring time, as they are not automatically updated when you update your typed SQL/ORM queries and depending on the typescript config you are using, it may not report additional properties being present as an error. Also I bet the return type annotation will make the compiler lie to the frontend about the true returned data type, which may lead to sensitive fields traveling through the network unnoticed. Reporting additional properties as an error might be something you don't want to turn on (at least I don't do it even if I inherit from @tsconfig/strictest).

There is also the case where you get weird return types from third party libs like

type User = {
    followers: User[] | null //This is null when the user has no followers. Replacing null by an array simplifies dealing with the field
    created_at: Date, //We want to stringify Dates
    last_active: Date | null //This is null if the user never logged in because our DB guy needed it for stats (users that registered but never logged in). Replacing null by the created_at field would be a sensible choice
}

You can actually do something like

return {
    ...user,
    followers: user.followers ?? [] satisfies User["followers"],
    last_active: user.last_active?.toString() ?? user.created_at.toString()
}

Or even wrap that logic in a function taking an User, but I find having a user_schema definition that does this same thing using zod way more readable and composable, specially if there are several endpoints that return objects of the User type coming from the same third party library/service/ORM.

Is this necessary in a world where you have access to getRequestEvent? Wouldn't you put your services on locals and access them there?

This will depend on the complexity of your backend. For low to medium complexity backends it would be enough, but as your backend starts needing to scale you will start encountering things that cannot be done properly or safely by defining a global and static set of services:

You may want to provide different service implementations (or service configurations) to different endpoints (redis cache VS in-memory cache, a per-endpoint set of rabbitmq queues or event buses with typed names and schemas, etc). Otherwise you will end with all rabbitmq queues, caches and event buses available to all endpoints, which results in a cluttered intellisense autocomplete and options that will certainly cause bugs being listed as available (ie: an immutable edge cache appearing as an option on an endpoint that returns variable data, events being emitted on the wrong event bus, etc.).

@Rich-Harris
Comment options

Both of these problems are better solved with normal functions

@rebasecase
Comment options

Whenever your function's output may depend on untrusted or dynamic inputs whose validity your type annotations cannot actually enforce at runtime.

Comment options

Is this pattern

let countryId = $derived(page.url.searchParams.get('countryId'))

let query = $derived(getUsers({ countryId }))
</script>

{#if query.loading}
  <p>Loading</p>
 {:else}
   {JSON.stringify(query.current)}
 {/if}

supposed to run on the client or SSR? I liked the experience for web-apps that we change the page right away, then have a skeleton and like 200-500ms later, the data comes in. It appears that currently this is being "SSR'd" (no loader visible). I'm not sure if it's a bug that I don't see a loader (due to recent SSR changes), or it's expected

You must be logged in to vote
1 reply
@jose-manuel-silva
Comment options

I have the exact same question, it currently SSRs unless you wrap the component/page where you are doing this with a boundary, which is not what I personally expected

But wrapping on boundary, makes the whole script run only on client (If I put a console log in the script, it only shows on browser console.log, without boundary it appears on vscode and browser)

I think this is not going to be the final design, and when Async SSR comes this will change

If someone from the team could confirm this, would be nice :)

EDIT:

Reading upon other comments in this thread, yes, it will be implemented in the future when Async SSR comes
You can do something like:

#13897 (comment)

For now

Comment options

I’m trying to figure out how to handle data flow with remote functions when using a separate backend (e.g., FastAPI). From what I can tell, .remote functions are missing some of the capabilities that +page.ts provides with SSR, specifically; There’s no way to access fetch in a context that runs on both server and client (as +page.ts does).

My use-case

I have SvelteKit running on http://localhost:3000 and FastAPI running on http://localhost:8000. The SvelteKit is served behind https://my-app.com and FastAPI behind https://api.my-app.com.

I don’t want to recreate every API endpoint in SvelteKit. Instead, I’d like to directly consume FastAPI’s endpoints:

  • On the server, requests should go to http://localhost:8000.
  • On the client, requests should go to https://api.my-app.com.

Using load functions

A fetch example:

// routes/+page.ts
export const load = async ({ fetch }) => {
    // SvelteKit’s `fetch` is intercepted by `handleFetch`.
    // On the server: request is rewritten to http://localhost:8000
    // On the client: request goes to https://api.my-app.com
    const result = await fetch("https://api.my-app.com/api/contact")
    const contact = await result.json()

    return { contact }
}

The rewrite is handled in hooks.server.ts:

// hooks.server.ts
export const handleFetch = async ({ event, fetch, request }) => {

    // If i am calling my API from the server, call the API server process directly instead.
    if (request.url.startsWith("https://api.my-app.com")) {
        request = new Request(
            request.url.replace("https://api.my-app.com", 'http://localhost:8000'),
            request
        )
        // ... and do more stuff regarding cookies and more ...
    }

    return fetch(request)
}

with .remote.ts functions

With .remote functions (without load), I can’t find a way to achieve the same behavior. I can’t access SvelteKit’s getRequestEvent().fetch in a universal server/client context. As a result, I can’t transparently swap between the local backend and the public API.

So currently I see two options:

  • Always call the public API (https://api.my-app.com) from the client.
  • Create a new .remote function in SvelteKit (duplicating the FastAPI endpoint) and fetch data SSR through that.

What is the recommended approach here? Is there a way to use .remote functions while still leveraging the same fetch/handleFetch mechanics as +page.ts?

Or do I need to adjust my approach; is the recommended practice to essentially re-create all of my API endpoints within SvelteKit using .remote functions?

You must be logged in to vote
8 replies
@OTheNonE
Comment options

That is true @KTibow, the utility function could look like this:

export const getApiUrl = () => browser ? "https://api.my-app.com" : "http://localhost:8000"

Although now the internal port number gets sent to the client (which probably isn't a problem). A "little" better solution would be that fetch written in client side code could be intercepted when rendered on server side, as is possible with +page.ts.

@KiraPC
Comment options

Remote function runs its logic just on server side, so you don't need to rewrite the url, you can just use localhost.

On the client side the remote function is just a fetch wrap to your SvelteKit server.

@OTheNonE
Comment options

And that client-side wrap makes a call to the belonging SvelteKit RPC endpoint, serving the same data as the public API already is serving. With that in mind, maybe i should instead ask:

In contrary to using the public endpoints of my API directly, is it recommended that i re-create the endpoints of my public API in my SvelteKit server using Remote Functions?

@KTibow
Comment options

Although now the internal port number gets sent to the client

Actually, it isn't! Google tree shaking.

@OTheNonE
Comment options

I had not thought about tree shaking, and it works when the urls are hardcoded! Although, i would not store the urls in code, but instead in environment variables:

PUBLIC_API_URL="https://api.my-app.com" 
SERVER_API_URL="http://localhost:8000"

And when i write this code:

import { env as public_env } from "$env/dynamic/public"
import { env as server_env } from "$env/dynamic/private"
import { browser } from "$app/environment";

export const getApiUrl = () => browser 
    ? public_env.PUBLIC_API_URL
    : server_env.SERVER_API_URL

The build fails... Even if it were to work, we surely should not rely on tree-shaking to remove secrets from client code? But then again in this specific case, is it even bad to expose the port number of the server process to the client?

Either way, this discussion could be resolved if only fetch'es written in client-side code could be intercepted by handleFetch when SSR'ed. The only way to access such a fetch is through the load function export const load = async ({ fetch }) => { ... } in +page.ts, and is not accessible in the current feature set of RF.

And then again, is all of this public/internal api url handling even necessary if you are not meant to call your backend api in the way that i describe above, but you should instead just wrap calls to the api in a RF and have the RF make an additional (duplicate) endpoint only for the SvelteKit app?

Comment options

Is it possible to get validation error when using command? I couldn't find any doc or implement myself.
How to get validation error on this example. It only returns Bad Request to cleint, no error detail.
export const addLike = command(v.string(), async (id) => {
await db.sqlUPDATE item SET likes = likes + 1 WHERE id = ${id};
});

You must be logged in to vote
2 replies
@dummdidumm
Comment options

dummdidumm Oct 1, 2025
Maintainer Author

Use the corresponding hook as described here: https://svelte.dev/docs/kit/remote-functions#Handling-validation-errors

Note that through this you will transform validation errors of all remote functions. If you want to instead specifically make it fail in a certain way on a per-remote-function basis, please let us know the use case so we can think about it.

@sbscan
Comment options

Yes, surely per remote function like the old API way, so I can inform the client which input is wrong. Now I have to validate the same on the client.

Comment options

When comparing load vs .remote, it seems like .remote is a replacement for +page.server.ts, while there is no replacement for +page.ts. My "not-thoroughly-thought-out" idea would be to have a .local.ts file with client side functions. These would not create new endpoints, and would be used for client side handling of data.

  • import { query } from "$app/client" : Would basically act as a wrapper around fetch with SvelteKit's power, also so that you have the same interface as when using RF's query.
  • import { form } from "$app/client": Used for client side handling of form submittion, especially useful for SPA's.
You must be logged in to vote
4 replies
@KiraPC
Comment options

Why do you feel the need for such a feature? What you’re describing can already be done with plain JavaScript on the client side. What extra value would this bring on top of that? Am I missing something here?

@KTibow
Comment options

I think "resources" will be the solution to this

@OTheNonE
Comment options

Aah yes, resources has been mentioned in the Svelte Async SSR discussion.

I just thought it would be convenient to have the same interface for fetching data on the client-side as on the server, although you're right, it's probably easy to implement yourself, and wrappers already exists for this. The only thing missing is for fetch to be captured by handleFetch when SSR'ed.

@davidbaranek
Comment options

I think there could be a use for that. For example, I’m using SvelteKit with an external API server, but I also use SvelteKit load functions for SSR. Right now, I’m using universal +page.ts load functions because there’s nothing secret in them. I like that the data is loaded server side during SSR and then client side when refetching, avoiding an extra step when reaching to my API.

If I understand correctly, remote functions will eventually replace load functions, which means that for SSR to work, I’ll have to use them. However, that would make every request go through the SvelteKit server instead of directly from the client to my API server.

It would be convenient to have “universal” remote functions that run on the server during SSR and on the client for refetching, etc.
Sorry if I’m misunderstanding something, I’m still a bit confused about how remote functions work.

Comment options

Is there a way either current or planned to cache queries that take no arguments? The documentation says that queries are cached based on their arguments. I have a query that takes no arguments and am seeing it make a second fetch call during hydration. My current workaround is to make the query accept an unused argument of v.object({}), and then call the query with that empty object everywhere.

export const getRankings = query(v.object({}), async () => rankings);
<script lang="ts">
  let rankings = await getRankings({});
</script>
You must be logged in to vote
3 replies
@hugo-hsi-dev
Comment options

Is it not currently cached as it should? If not, I’d guess it’s a bug.

@Rich-Harris
Comment options

yes, please file an issue

@dummdidumm
Comment options

dummdidumm Oct 1, 2025
Maintainer Author

fixed by #14563

Comment options

If i have a getPosts() query, that returns an array of all the posts in the db, how can I add a record to its result without retrieving everything from the db again when I run the createPost() form?

My create post API returns the INSERT result leveraging RETURNING, so I don’t need to refresh everything. I saw .set() in the documentation, but that seems to replace the result, is there a way to append?

You must be logged in to vote
2 replies
@redmamoth
Comment options

I had hoped this would work:

await getPosts().set([createResult.post!, ...getPosts().current ?? []]);

But it seems not.

@phi-bre
Comment options

Where are you calling await getPosts().set([createResult.post!, ...getPosts().current ?? []])? In a component or inside the createPost?

Comment options

A feature I would like to see is the ability to get reactive state from a query. The caching of queries would make sharing state between components very easy.

One can currently get this result by using a class and a transport hook.

// ReactiveArray.svelte.ts
export class ReactiveArray {
  array

  constructor(array: number[]) {
    this.array = $state(array)
  }

  encode() {
    return this.array
  }
}
// hooks.ts
export const transport: Transport = {
  ReactiveArray: {
    encode: (value) => value instanceof ReactiveArray && value.encode(),
    decode: (data) => new ReactiveArray(data)
  }
}
// query.remote.ts
import { query } from "$app/server";
import { ReactiveArray } from "./ReactiveArray.svelte";

export const getStateTest = query(() => {
  return new ReactiveArray([1, 2, 3, 4, 5])
})

Using this query like so...

<script lang="ts">
  import { getStateTest } from "$lib/stateTesting.remote";

  let data1 = $derived(await getStateTest());
  let data2 = $derived(await getStateTest());
</script>

<h1>Data 1</h1>
<div class="flex flex-col">
  {#each data1.array, i (i)}
    <input bind:value={data1.array[i]} type="number" />
  {/each}
</div>

<h1>Data 2</h1>
<div class="flex flex-col">
  {#each data2.array, i (i)}
    <input bind:value={data2.array[i]} type="number" />
  {/each}
</div>

<button onclick={() => {getStateTest().refresh()}}>refresh</button>

...will give inputs which are always kept in sync. Refreshing the query will update the state to whatever the query returns.

I think this behaviour would be nice to have as an opt-in. Just wrapping the result of the query in a $state() would go a long way. But this needs to be at the cache level which as far as I know is not possible with the current API.

Maybe opting in whenever the query is used with something like getStateTest().reactive() could be a way to do it? This would then return a state proxy (hope I'm using the right terminology here 😄!) of whatever the query returned originally.

You must be logged in to vote
0 replies
Comment options

is is possible to select only some of the data from query().
something like on @tanstack/svelte-query useQuery({
queryKey: ['something'],
queryFn: async () => { id: 1, title: "blah blah..", description: "" },
select: (value) => ({ id: value.id, title: value.title })
});

You must be logged in to vote
2 replies
@Rich-Harris
Comment options

No need to have a special API for it, just use function composition:

function select(value) {
  return { id: value.id, title: value.title };
}

// later
const selection = select(await myQuery());

Or if you need to have it at the definition site:

function select(fn, selector) {
  return async (arg) => selector(await fn(arg));
}

const myQueryWithSelector = select(myQuery, (value) => ({ id: value.id, title: value.title }));

const selection = await myQueryWithSelector();

Really, though, you're better off just creating a new remote function so that you don't need to send that data to the client in the first place:

export const myQuery = query(schema, (data) => {...});

export const myQueryWithSelector = query(schema, async (data) => {
  const value = await myQuery(data);
  return { id: value.id, title: value.title };
});
@sharmapukar217
Comment options

this would create a new query, and for optimisic update, do i need to update myQueryWithSelector or updating myQuery would auto update myQueryWithSelector as well?

Comment options

I tried using the form() API with an invoice object that includes items (invoice entries).
I ran into issues handling arrays / arrays of objects — and eventually migrated everything back to command(), because it wasn’t clear how to make it work properly with dynamic lists.

On the Svelte Discord, someone suggested this example:

<script lang="ts">
	import { test } from "./form.remote";

	let values = $state({ test: [""] });
	test.fields.set(values);
</script>

<form {...test}>
	<p>
		<button type="button" onclick={() => values.test.push("")}>Add</button>
	</p>
	{#each values.test, i (i)}
		<p>
			<input {...test.fields.test[i]?.as("text")} />
			<button type="button" onclick={() => values.test.splice(i, 1)}>Remove</button>
		</p>
	{/each}
	<button type="submit">Submit</button>
</form>

This approach does work, but it quickly becomes messy in larger forms — especially when dealing with nested arrays or objects (e.g. invoice items, line entries, etc.). The manual $state() setup and managing reactivity for nested structures makes it feel cumbersome compared to how simple form() otherwise is.

Additionally, setting existing data feels awkward. Everything about form() is simple and declarative until you need to manually create and assign a state object to initialize existing values. That part breaks the flow a bit.

My forms are normally looking like this:

... 
let userProfileQuery = $derived(getUserProfile())
</script>

{#if userProfileQuery.loading && !userProfileQuery.current}
  <MyLoaderComponent />
{:else if userProfileQuery.current}
  <UpdateUserProfile data={userProfileQuery.current} />
{/if}

Form itself

// form
let { userProfile } = props();

updateUserProfile.fields.set({
  firstName: userProfile.firstName,
  lastName: userProfile.lastName,
  phone: userProfile.phone
});

// or just updateUserProfile.fields.set(userProfile)
</script>

<form {...updateUserProfile}> <!-- or with .enhance() -->
  <input ... />
</form>

This felt easy and simple.

You must be logged in to vote
0 replies
Comment options

is there a way to manually reset the form state?

scenario: i have a form in a modal that creates items. when submitting the modal closes but when the user goes back to the form, they still see the "success" message from result.success, i'd like to fully clear the form state on the close of the modal but can only seem to rest the form values using enhance and form.reset()

You must be logged in to vote
1 reply
@theetrain
Comment options

There's a PR open that aims to provide reset functions: #14779.

There may be existing workarounds in the linked issues: #14210 (comment).

Comment options

#13897 (comment)
Does anyone know of a way to achieve this?

You must be logged in to vote
0 replies
Comment options

RE: Streaming use cases

My company deals with stocks, and on many of our pages we tend to have multiple streaming stock options. We've rigged up a solution with an external API, but if we could get something setup where we could use SSE natively in sveltekit that would be preferred.

You must be logged in to vote
1 reply
@Rafterman1217
Comment options

There is a proposal

#14292

Would love to see this implemented!

Comment options

is there any way to set or get global cache so that something like tanstack/query-devtool can be implemented?

You must be logged in to vote
0 replies
Comment options

did the behavior of assigning a query result to a variable in the <script> tag recently change?

before it seemed to be non-blocking if you don't await but after an update it seems to now block the render of the page. this makes it so it's impossible to use the const query = getPosts(); if you don't want it to block without having to resort to using a <svelte:boundary>.

the docs don't make this clear as to when things happen (client vs server) so at least a note on this would be useful to add clarity.

related, why is the {#each await getPosts() as { title, slug }} recommended? it seems less robust; it can only access loading/error state within a <svelte:boundary> vs having the state global to the component where you could selectively use error/loading states (for example to disable a button you want always rendered)

You must be logged in to vote
9 replies
@jeffpohlmeyer
Comment options

i can give that a shot but feels very hacky. i would assume a lot of people that will use query will do so in-part because they have some slow query they want to run without just stuffing it into a load function.

the await syntax allows for client-first loading which is great, but the UX around await lacks in comparison to assigning a promise and using the methods on the query. also, having to wrap ever call to query in a <svelte:boundary> is clunky and requires a lot of boilerplate.

a global boundary of course could handle things in the most simple of cases but i'm sure many people want/need loading/error states all over their apps in varous sub-components. having to do a boundary for every component vs a simple conditional would end up getting painful real quick, vs just using the query.loading and query.error methods which are much more user friendly

one thing to add that i ran into is in testing this with JS disabled. i know, i know, the majority of users will have JS enabled, but there's still a part of me that wants to handle edge cases. i seem to recall seeing something where they're working on this, but if a user doesn't properly download the bundle an SSR'd page with a query with <svelte:boundary> still only renders the pending snippet

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

Can you provide a minimal repro of this behavior?

@dangodai
Comment options

related, why is the {#each await getPosts() as { title, slug }} recommended?

I find that most of the examples given in the Github discussions (async, remote functions) and the docs follow this approach. But unfortunately this approach, for me at least, only works in these tidy little examples and not in real life applications.

Awaiting the same promises for every property you need out of them is unpleasant (both having to write await, and having to wrap it in brackets, e.g. {#if (await foo).bar} {(await foo).bar.baz)} {/if}). (Edit: actually this doesn't work, as awaiting the promise twice can't narrow the types, kind of at a loss for what's expected in this situation)

Moving the await into the script tag moves the async work outside of any boundary declared in the component, so you would effectively need two copies of every async component that needs a boundary: an inner one which is your real component, and an outer one which just wraps the inner component in a boundary.

Another option is to use {@const await ...}, but frankly I find this to be gross DX. I would like to keep my state and logic disentangled from my markup with the unavoidable exception of each blocks.

Like you I've settled so far on kicking off my promises in the script and creating $derived promises for all of the data I need out of them. I still have to litter await all over my template, but it solves the gross property access pattern. Not great, but it works okay. Really I just think it should be possible to wrap an entire component, script and all, in a boundary. Otherwise top level await in script tags has fairly limited usefulness if you care about precise boundaries.

@danawoodman
Comment options

created an example repo of the two approaches i'm aware of and the pros/cons

https://github.com/danawoodman/svelte-kit-remote-funcs-ux

I've also noticed that my script example is somehow breaking navigation, which is an unexpected bug despite the page working fine and no errors showing up

@yayza
Comment options

created an example repo of the two approaches i'm aware of and the pros/cons

https://github.com/danawoodman/svelte-kit-remote-funcs-ux

I've also noticed that my script example is somehow breaking navigation, which is an unexpected bug despite the page working fine and no errors showing up

thank you for this, was wondering how to use queries in the script tag without blocking page load so i can pass them to other parts of the script.

my current way was to create child components and render them inside the boundary and passing them the result of the boundary async. now i can move that logic back to parent and delete that child one.

Comment options

                   <Label class="flex flex-col gap-2 items-start">
						<span class="ml-0.5">Salutation</span>
						<Combobox
							placeholder="Choose a salutation ..."
							options={['Mr.', 'Ms.', 'Dr.'].map((s) => ({ label: s, value: s }))}
							onselect={(val) => {
								createBusiness.fields.contacts[i].salutation.set(val as 'Mr.');
							}}
						/>
						{#each createBusiness.fields.contacts[i].salutation.issues() as issue}
							<p>{issue.message}</p>
						{/each}
					</Label>

Salutation is z.enum(['Mr.', 'Ms.', 'Dr.']). Why do i still see the invalid option issue when I submit the form even after explicitly setting the fields value.
Also is there a way to just bind the value of my custom combobox component to the form's field value?

You must be logged in to vote
1 reply
@skaldebane
Comment options

I've had the same issue with shadcn combobox as well. The solution I ended up with is adding a hidden <select> where I spread the exact same field, which seemed to solve the "invalid" issue.

This definitely should be improved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Morty Proxy This is a proxified and sanitized view of the page, visit original site.