diff --git a/apps/svelte.dev/content/docs/kit/20-core-concepts/60-remote-functions.md b/apps/svelte.dev/content/docs/kit/20-core-concepts/60-remote-functions.md new file mode 100644 index 000000000..0e744641a --- /dev/null +++ b/apps/svelte.dev/content/docs/kit/20-core-concepts/60-remote-functions.md @@ -0,0 +1,471 @@ +--- +NOTE: do not edit this file, it is generated in apps/svelte.dev/scripts/sync-docs/index.ts +title: Remote Functions +--- + +Remote functions are a new concept in SvelteKit since version 2.27 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`. Combined with Svelte's [experimental async feature](/docs/svelte/await-expressions) it allows you to load and manipulate date directly inside your components. If you're familiar with RPC and 'server functions', this is basically our take on the concept. + +This feature is currently experimental, and you must opt in by adding the `kit.experimental.remoteFunctions` option in your `svelte.config.js`: + +```js +/// file: svelte.config.js +export default { + kit: { + experimental: { + remoteFunctions: true + } + } +}; +``` + +## Overview + +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. + +As of now there exist 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](#Validation) section). The argument is serialized with [devalue](https://github.com/rich-harris/devalue), which handles types like `Date` and `Map` in addition to JSON, and takes the [transport hook](https://svelte.dev/docs/kit/hooks#Universal-hooks-transport) into account. + +```ts +/// file: likes.remote.ts +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. + +```svelte + + + +
likes: {await getLikes(item.id)}
+``` + +> 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 `status` and `current` (which contains the most recent value, but is initially `undefined`) and methods like `withOverride(...)` (see the section on [optimistic UI](#Optimistic-updates), 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. Use the remote `form` function to achieve this: + +```ts +/// file: likes.remote.ts +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 ` + +likes: {await getLikes(item.id)}
+``` + +By default, all queries used on the page (along with any `load` functions) are automatically refreshed following a form submission, meaning `getLikes(...)` in the example above will show updated data. + +In addition to the enumerable properties, remote forms (`addLike` in our example) have non-enumerable properties. One of them is `result` which contains the return value. Use it to display something in response to the submission. + +```svelte + + + ++++{#if addLike.result?.success} +success!
+{/if}+++ + + + +likes: {await getLikes(item.id)}
+``` + +### enhance + +The remote form property `enhance` 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): + +```svelte + + + +{#if addLike.result?.success} +success!
+{/if} + + + +likes: {await getLikes(item.id)}
+``` + +> `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](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(...)`: + +```ts +/// file: likes.remote.ts +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 }; +}); +``` + +### Forms in a list + +Sometimes you may have form submissions of the same type in a list, and each form action should be independently managed. Use the `for` method to create separate instances of the same form submission. In the following example, each todo item gets a separate `toggleTodo` form instance, which means you can toggle many in quick succession, with the submission results staying independent of each other: + +```svelte + + + +{#each await getTodos() as todo (todo.id)} + {@const toggle = toggleTodo.for(todo.id)} + +{/each} +``` + +### formAction + +Forms allow you to have more than one button that kicks up a form submission. The non-primary buttons must have a `formaction` property in that case. The remote form provides a `formAction` property that allows you to do just that: + +```svelte + + + + +``` + +## command + +For cases where serving no-JS users via the remote `form` function is impractical or undesirable, `command` offers an alternative way to write data to the server. + +```ts +/// file: likes.remote.ts +import z from 'zod'; +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 = 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 }; +}); +``` + +Now simply call `addLike`, from (for example) an event handler: + +```svelte + + + + + +likes: {await getLikes(item.id)}
+``` + +> 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. + +```ts +/// file: blog.remote.ts +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, and will be put into the user's browser cache using the [Cache API](https://developer.mozilla.org/en-US/docs/Web/API/Cache) which even survives page reloads. + +> 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: + +```ts +/// file: blog.remote.ts +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: + +```ts +/// file: blog.remote.ts +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(...)`: + +```svelte + + + + + +likes: {await getLikes(item.id)}
+``` + +> You can also do `const likes = $derived(getLikes(item.id))` in your ` + + + +``` +Use the `updates` method to specify which queries to update in response to the command. +```svelte + + + + + +{post.content}
+ +``` + +@@ -35,6 +146,144 @@ function getRequestEvent(): RequestEvent< +## prerender + +Creates a prerendered remote function. The given function is invoked at build time and the result is stored to disk. +```ts +import { blogPosts } from '$lib/server/db'; + +export const blogPosts = prerender(() => blogPosts.getAll()); +``` + +In case your function has an argument, you need to provide an `inputs` function that returns a list representing the arguments to be used for prerendering. +```ts +import z from 'zod'; +import { blogPosts } from '$lib/server/db'; + +export const blogPost = prerender( + z.string(), + (id) => blogPosts.get(id), + { inputs: () => blogPosts.getAll().map((post) => post.id) } +); +``` + ++ +```dts +function prerender+ ++ +```dts +function prerender( + validate: 'unchecked', + fn: (arg: Input) => MaybePromise+ +, + options?: + | { + inputs?: RemotePrerenderInputsGenerator; + dynamic?: boolean; + } + | undefined +): RemotePrerenderFunction; +``` + + + +```dts +function prerender+ + + +## query + +Creates a remote function that can be invoked like a regular function within components. +The given function is invoked directly on the backend and via a fetch call on the client. +```ts +import { blogPosts } from '$lib/server/db'; + +export const blogPosts = query(() => blogPosts.getAll()); +``` +```svelte + + +{#await blogPosts() then posts} + +{/await} +``` + +( + schema: Schema, + fn: ( + arg: StandardSchemaV1.InferOutput + ) => MaybePromise , + options?: + | { + inputs?: RemotePrerenderInputsGenerator< + StandardSchemaV1.InferOutput + >; + dynamic?: boolean; + } + | undefined +): RemotePrerenderFunction< + StandardSchemaV1.InferOutput , + Output +>; +``` + + + +```dts +function query+ +( + fn: () => MaybePromise +): RemoteQueryFunction ; +``` + + + +```dts +function query( + validate: 'unchecked', + fn: (arg: Input) => MaybePromise+ ++): RemoteQueryFunction; +``` + + + +```dts +function query+ + + ## read( + schema: Schema, + fn: ( + arg: StandardSchemaV1.InferOutput + ) => MaybePromise +): RemoteQueryFunction< + StandardSchemaV1.InferOutput , + Output +>; +``` + + diff --git a/apps/svelte.dev/content/docs/kit/98-reference/50-configuration.md b/apps/svelte.dev/content/docs/kit/98-reference/50-configuration.md index f84f417d4..dc6bf1552 100644 --- a/apps/svelte.dev/content/docs/kit/98-reference/50-configuration.md +++ b/apps/svelte.dev/content/docs/kit/98-reference/50-configuration.md @@ -359,6 +359,40 @@ A prefix that signals that an environment variable is unsafe to expose to client
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: