Skip to content

Commit eab3e1e

Browse files
github-actions[bot]svelte-docs-bot[bot]elliott-with-the-longest-name-on-github
authored
Sync kit docs (#1910)
* sync kit docs * update kit --------- Co-authored-by: svelte-docs-bot[bot] <196124396+svelte-docs-bot[bot]@users.noreply.github.com> Co-authored-by: Elliott Johnson <[email protected]>
1 parent 7989a5d commit eab3e1e

5 files changed

Lines changed: 195 additions & 171 deletions

File tree

apps/svelte.dev/content/docs/kit/20-core-concepts/60-remote-functions.md

Lines changed: 100 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -624,54 +624,6 @@ You can prevent sensitive data (such as passwords and credit card numbers) from
624624
625625
In this example, if the data does not validate, only the first `<input>` will be populated when the page reloads.
626626
627-
### Single-flight mutations
628-
629-
By default, all queries used on the page (along with any `load` functions) are automatically refreshed following a successful form submission. This ensures that everything is up-to-date, but it's also inefficient: many queries will be unchanged, and it requires a second trip to the server to get the updated data.
630-
631-
Instead, we can specify which queries should be refreshed in response to a particular form submission. This is called a _single-flight mutation_, and there are two ways to achieve it. The first is to refresh the query on the server, inside the form handler:
632-
633-
```js
634-
import * as v from 'valibot';
635-
import { error, redirect } from '@sveltejs/kit';
636-
import { query, form } from '$app/server';
637-
const slug = '';
638-
const post = { id: '' };
639-
/** @type {any} */
640-
const externalApi = '';
641-
// ---cut---
642-
export const getPosts = query(async () => { /* ... */ });
643-
644-
export const getPost = query(v.string(), async (slug) => { /* ... */ });
645-
646-
export const createPost = form(
647-
v.object({/* ... */}),
648-
async (data) => {
649-
// form logic goes here...
650-
651-
// Refresh `getPosts()` on the server, and send
652-
// the data back with the result of `createPost`
653-
+++await getPosts().refresh();+++
654-
655-
// Redirect to the newly created page
656-
redirect(303, `/blog/${slug}`);
657-
}
658-
);
659-
660-
export const updatePost = form(
661-
v.object({/* ... */}),
662-
async (data) => {
663-
// form logic goes here...
664-
const result = externalApi.update(post);
665-
666-
// The API already gives us the updated post,
667-
// no need to refresh it, we can set it directly
668-
+++await getPost(post.id).set(result);+++
669-
}
670-
);
671-
```
672-
673-
The second is to drive the single-flight mutation from the client, which we'll see in the section on [`enhance`](#form-enhance).
674-
675627
### Returns and redirects
676628
677629
The example above uses [`redirect(...)`](@sveltejs-kit#redirect), which sends the user to the newly created page. Alternatively, the callback could return data, in which case it would be available as `createPost.result`:
@@ -769,39 +721,6 @@ We can customize what happens when the form is submitted with the `enhance` meth
769721
770722
The callback receives the `form` element, the `data` it contains, and a `submit` function.
771723
772-
To enable client-driven [single-flight mutations](#form-Single-flight-mutations), use `submit().updates(...)`. For example, if the `getPosts()` query was used on this page, we could refresh it like so:
773-
774-
```ts
775-
import type { RemoteQuery, RemoteQueryOverride } from '@sveltejs/kit';
776-
interface Post {}
777-
declare function submit(): Promise<any> & {
778-
updates(...queries: Array<RemoteQuery<any> | RemoteQueryOverride>): Promise<any>;
779-
}
780-
781-
declare function getPosts(): RemoteQuery<Post[]>;
782-
// ---cut---
783-
await submit().updates(getPosts());
784-
```
785-
786-
We can also _override_ the current data while the submission is ongoing:
787-
788-
```ts
789-
import type { RemoteQuery, RemoteQueryOverride } from '@sveltejs/kit';
790-
interface Post {}
791-
declare function submit(): Promise<any> & {
792-
updates(...queries: Array<RemoteQuery<any> | RemoteQueryOverride>): Promise<any>;
793-
}
794-
795-
declare function getPosts(): RemoteQuery<Post[]>;
796-
declare const newPost: Post;
797-
// ---cut---
798-
await submit().updates(
799-
getPosts().withOverride((posts) => [newPost, ...posts])
800-
);
801-
```
802-
803-
The override will be applied immediately, and released when the submission completes (or fails).
804-
805724
### Multiple instances of a form
806725
807726
Some forms may be repeated as part of a list. In this case you can create separate instances of a form function via `for(id)` to achieve isolation.
@@ -943,78 +862,126 @@ Now simply call `addLike`, from (for example) an event handler:
943862
944863
> [!NOTE] Commands cannot be called during render.
945864
946-
### Updating queries
865+
## Single-flight mutations
866+
867+
The purpose of both [`form`](#form) and [`command`](#command) is *mutating data*. In many cases, mutating data invalidates other data. By default, `form` deals with this by automatically invalidating all queries and load functions following a successful submission, to emulate what would happen with a traditional full-page reload. `command`, on the other hand, does nothing. Typically, neither of these options is going to be the ideal solution — invalidating everything is likely wasteful, as it's unlikely a form submission changed *everything* being displayed on your webpage. In the case of `command`, doing nothing likely *under*-invalidates your app, leaving stale data displayed. In both cases, it's common to have to perform two round-trips to the server: One to run the mutation, and another after that completes to re-request the data from any queries you need to refresh.
947868
948-
To update `getLikes(item.id)`, or any other query, we need to tell SvelteKit _which_ queries need to be refreshed (unlike `form`, which by default invalidates everything, to approximate the behaviour of a native form submission).
869+
SvelteKit solves both of these problems with *single-flight mutations*: Your `form` submission or `command` invocation can refresh queries and pass their results back to the client in a single request.
949870
950-
We either do that inside the command itself...
871+
### Server-driven refreshes
872+
873+
In most circumstances, the server handler knows what client data needs to be updated based on its arguments:
951874
952875
```js
953-
/// file: likes.remote.js
954-
// @filename: ambient.d.ts
955-
declare module '$lib/server/database' {
956-
export function sql(strings: TemplateStringsArray, ...values: any[]): Promise<any[]>;
957-
}
958-
// @filename: index.js
959-
// ---cut---
960876
import * as v from 'valibot';
961-
import { query, command } from '$app/server';
962-
import * as db from '$lib/server/database';
877+
import { error, redirect } from '@sveltejs/kit';
878+
import { query, form } from '$app/server';
879+
const slug = '';
880+
const post = { id: '' };
881+
/** @type {any} */
882+
const externalApi = '';
963883
// ---cut---
964-
export const getLikes = query(v.string(), async (id) => { /* ... */ });
884+
export const getPosts = query(async () => { /* ... */ });
965885

966-
export const addLike = command(v.string(), async (id) => {
967-
await db.sql`
968-
UPDATE item
969-
SET likes = likes + 1
970-
WHERE id = ${id}
971-
`;
886+
export const getPost = query(v.string(), async (slug) => { /* ... */ });
972887

973-
+++getLikes(id).refresh();+++
974-
// Just like within form functions you can also do
975-
// getLikes(id).set(...)
976-
// in case you have the result already
977-
});
888+
export const createPost = form(
889+
v.object({/* ... */}),
890+
async (data) => {
891+
// form logic goes here...
892+
893+
// Refresh `getPosts()` on the server, and send
894+
// the data back with the result of `createPost`
895+
// it's safe to throw away the promise from `refresh`,
896+
// as the framework awaits it for us before serving the response
897+
+++void getPosts().refresh();+++
898+
899+
// Redirect to the newly created page
900+
redirect(303, `/blog/${slug}`);
901+
}
902+
);
903+
904+
export const updatePost = form(
905+
v.object({ id: v.string() }),
906+
async (post) => {
907+
// form logic goes here...
908+
const result = externalApi.update(post);
909+
910+
// The API already gives us the updated post,
911+
// no need to refresh it, we can set it directly
912+
+++getPost(post.id).set(result);+++
913+
}
914+
);
978915
```
979916
980-
...or when we call it:
917+
Because queries are keyed based on their arguments, `await getPost(post.id).set(result)` on the server knows to look up the matching `getPost(id)` on the client to update it. The same goes for `getPosts().refresh()` -- it knows to look up `getPosts()` with no argument on the client.
981918
982-
```ts
983-
import { RemoteCommand, RemoteQueryFunction } from '@sveltejs/kit';
919+
### Client-requested refreshes
984920
985-
interface Item { id: string }
921+
Unfortunately, life isn't always as simple as the preceding example. The server always knows which query _functions_ to update, but it may not know which specific query _instances_ to update. For example, if `getPosts({ filter: 'author:santa' })` is rendered on the client, calling `getPosts().refresh()` in the server handler won't update it. You'd need to call `getPosts({ filter: 'author:santa' }).refresh()` instead — but how could you know which specific combinations of filters are currently rendered on the client, especially if your query argument is more complicated than an object with just one key?
986922
987-
declare const addLike: RemoteCommand<string, void>;
988-
declare const getLikes: RemoteQueryFunction<string, number>;
989-
declare function showToast(message: string): void;
990-
declare const item: Item;
991-
// ---cut---
992-
try {
993-
await addLike(item.id).+++updates(getLikes(item.id))+++;
994-
} catch (error) {
995-
showToast('Something went wrong!');
923+
SvelteKit makes this easy by allowing the client to _request_ that the server updates specific data using `submit().updates` (for `form`) or `myCommand().updates` (for `command`):
924+
925+
```ts
926+
import type { RemoteQueryUpdate, RemoteQuery } from '@sveltejs/kit';
927+
interface Post {}
928+
declare function submit(): Promise<any> & {
929+
updates(...updates: RemoteQueryUpdate[]): Promise<any>;
996930
}
931+
932+
declare function getPosts(args: { filter: string }): RemoteQuery<Post[]>;
933+
declare const newPost: Post;
934+
// ---cut---
935+
await submit().updates(
936+
// to request all active instances of getPosts
937+
getPosts,
938+
// to request a specific instance
939+
getPosts({ filter: 'author:santa' }),
940+
// to request a specific instance with an optimistic override
941+
getPosts({ filter: 'author:santa' }).withOverride((posts) => [newPost, ...posts])
942+
);
997943
```
998944
999-
As before, we can use `withOverride` for optimistic updates:
945+
It's not enough to just request the updates from the client -- you need to accept them from the server as well:
1000946
1001-
```ts
1002-
import { RemoteCommand, RemoteQueryFunction } from '@sveltejs/kit';
947+
```js
948+
import * as v from 'valibot';
949+
import { error, redirect } from '@sveltejs/kit';
950+
const slug = '';
951+
const post = { id: '' };
952+
/** @type {any} */
953+
const externalApi = '';
954+
// ---cut---
955+
import { query, form, requested } from '$app/server';
956+
957+
export const getPosts = query(v.object({ filter: v.string() }), async ({ filter }) => { /* ... */ });
958+
959+
export const createPost = form(
960+
v.object({/* ... */}),
961+
async (data) => {
962+
// form logic goes here...
963+
964+
+++for (const arg of requested(getPosts, 1)) {+++
965+
+++ void getPosts(arg).refresh();+++
966+
+++}+++
1003967

1004-
interface Item { id: string }
968+
// Redirect to the newly created page
969+
redirect(303, `/blog/${slug}`);
970+
}
971+
);
972+
```
1005973
1006-
declare const addLike: RemoteCommand<string, void>;
1007-
declare const getLikes: RemoteQueryFunction<string, number>;
1008-
declare function showToast(message: string): void;
1009-
declare const item: Item;
974+
`requested` gives you access to the requested query arguments for the supplied query. It returns the *parsed* arguments for the query -- when these arguments are passed back into the query in `getPosts(arg).refresh()`, they will not be parsed again. If parsing an argument fails, that query will error, but the entire command will not fail. `requested`'s second parameter, `limit`, is the maximum number of items it will return. Any refresh requests beyond this limit will fail.
975+
976+
Additionally, `requested` allows a simple shorthand when all you want to do is refresh the requested query instances:
977+
978+
```ts
979+
import type { RemoteQueryFunction } from '@sveltejs/kit';
980+
import { requested } from '$app/server';
981+
declare const getPosts: RemoteQueryFunction<any, any>;
1010982
// ---cut---
1011-
try {
1012-
await addLike(item.id).updates(
1013-
getLikes(item.id).+++withOverride((n) => n + 1)+++
1014-
);
1015-
} catch (error) {
1016-
showToast('Something went wrong!');
1017-
}
983+
// this is the same as looping over the result and calling `void getPosts(arg).refresh()`.
984+
await requested(getPosts, 1).refreshAll();
1018985
```
1019986
1020987
## prerender

apps/svelte.dev/content/docs/kit/98-reference/[email protected]

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2397,9 +2397,7 @@ type RemoteCommand<Input, Output> = {
23972397
arg: undefined extends Input ? Input | void : Input
23982398
): Promise<Output> & {
23992399
updates(
2400-
...queries: Array<
2401-
RemoteQuery<any> | RemoteQueryOverride
2402-
>
2400+
...updates: RemoteQueryUpdate[]
24032401
): Promise<Output>;
24042402
};
24052403
/** The number of pending command executions */
@@ -2432,9 +2430,7 @@ type RemoteForm<
24322430
data: Input;
24332431
submit: () => Promise<void> & {
24342432
updates: (
2435-
...queries: Array<
2436-
RemoteQuery<any> | RemoteQueryOverride
2437-
>
2433+
...updates: RemoteQueryUpdate[]
24382434
) => Promise<void>;
24392435
};
24402436
}) => void | Promise<void>
@@ -2654,7 +2650,7 @@ type RemoteQuery<T> = RemoteResource<T> & {
26542650
*/
26552651
refresh(): Promise<void>;
26562652
/**
2657-
* Temporarily override the value of a query. This is used with the `updates` method of a [command](https://svelte.dev/docs/kit/remote-functions#command-Updating-queries) or [enhanced form submission](https://svelte.dev/docs/kit/remote-functions#form-enhance) to provide optimistic updates.
2653+
* Temporarily override a query's value during a [single-flight mutation](https://svelte.dev/docs/kit/remote-functions#Single-flight-mutations) to provide optimistic updates.
26582654
*
26592655
* ```svelte
26602656
* <script>
@@ -2699,26 +2695,23 @@ type RemoteQueryFunction<Input, Output> = (
26992695
<div class="ts-block">
27002696

27012697
```dts
2702-
interface RemoteQueryOverride {/*…*/}
2698+
type RemoteQueryOverride = () => void;
27032699
```
27042700

2705-
<div class="ts-block-property">
2706-
2707-
```dts
2708-
_key: string;
2709-
```
2710-
2711-
<div class="ts-block-property-details"></div>
27122701
</div>
27132702

2714-
<div class="ts-block-property">
2703+
## RemoteQueryUpdate
2704+
2705+
<div class="ts-block">
27152706

27162707
```dts
2717-
release(): void;
2708+
type RemoteQueryUpdate =
2709+
| RemoteQuery<any>
2710+
| RemoteQueryFunction<any, any>
2711+
| RemoteQueryOverride;
27182712
```
27192713

2720-
<div class="ts-block-property-details"></div>
2721-
</div></div>
2714+
</div>
27222715

27232716
## RemoteResource
27242717

@@ -3046,6 +3039,30 @@ type RequestHandler<
30463039

30473040
</div>
30483041

3042+
## RequestedResult
3043+
3044+
<div class="ts-block">
3045+
3046+
```dts
3047+
type RequestedResult<T> = Iterable<T> &
3048+
AsyncIterable<T> & {
3049+
/**
3050+
* Call `refresh` on all queries selected by this `requested` invocation.
3051+
* This is identical to:
3052+
* ```ts
3053+
* import { requested } from '$app/server';
3054+
*
3055+
* for await (const arg of requested(query, ...) {
3056+
* void query(arg).refresh();
3057+
* }
3058+
* ```
3059+
*/
3060+
refreshAll: () => Promise<void>;
3061+
};
3062+
```
3063+
3064+
</div>
3065+
30493066
## Reroute
30503067

30513068
<blockquote class="since note">

0 commit comments

Comments
 (0)