Gestion de l’état
Si vous êtes habitué•e•s à construire des applications web uniquement côté client, la gestion de l’état dans une application qui s’étend à la fois sur le serveur et sur le client peut paraître intimidante. Cette section fournit des astuces pour éviter quelques pièges classiques.
Éviter les états partagés sur le serveur
Les navigateurs sont stateful, c-à-d qu’ils sont capables de gérer de l’état — l’état est stocké en mémoire au fur et à mesure que l’utilisateur ou utilisatrice interagit avec l’application. Les serveurs, en revanche, sont stateless, c-à-d qu’ils ne peuvent pas gérer de l’état — le contenu d’une réponse est entièrement déterminé par le contenu de la requête.
En tout cas, conceptuellement. En réalité, les serveurs sont souvent des processus qui restent actifs longtemps et qui sont partagés par de nombreuses personnes les consommant. Pour cette raison, il est important de ne pas y stocker de données dans des varibles partagées. Par exemple, considérez ce bout de code :
let let user: any
user;
/** @type {import('./$types').PageServerLoad} */
export function function load(): {
user: any;
}
load() {
return { user: any
user };
}
/** @satisfies {import('./$types').Actions} */
export const const actions: {
default: ({ request }: {
request: any;
}) => Promise<void>;
}
actions = {
default: ({ request }: {
request: any;
}) => Promise<void>
default: async ({ request: any
request }) => {
const const data: any
data = await request: any
request.formData();
// NE FAITES JAMAIS ÇA !
let user: any
user = {
name: any
name: const data: any
data.get('name'),
embarrassingSecret: any
embarrassingSecret: const data: any
data.get('secret')
};
}
}
import type { type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad, type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions } from './$types';
let let user: any
user;
export const const load: PageServerLoad
load: type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad = () => {
return { user: any
user };
};
export const const actions: {
default: ({ request }: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>;
}
actions = {
default: ({ request }: Kit.RequestEvent<Record<string, any>, string | null>) => Promise<void>
default: async ({ request: Request
The original request object.
request }) => {
const const data: FormData
data = await request: Request
The original request object.
request.Body.formData(): Promise<FormData>
formData();
// NE FAITES JAMAIS ÇA !
let user: any
user = {
name: FormDataEntryValue | null
name: const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('name'),
embarrassingSecret: FormDataEntryValue | null
embarrassingSecret: const data: FormData
data.FormData.get(name: string): FormDataEntryValue | null
get('secret')
};
}
} satisfies type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
type Actions = {
[x: string]: Kit.Action<Record<string, any>, void | Record<string, any>, string | null>;
}
Actions
La variable user
est partagée — donc accessible — par toute personne se connectant au serveur. Si
Alice a envoyé un secret embarrassant, et Bob visite la page un peu après, Bob va être au courant du
secret d’Alice. De plus, lorsqu’Alice va revenir sur le site plus tard dans la journée, le serveur
peut avoir été réinitialisé, perdant ses données.
Vous devriez plutôt authentifier l’utilisateur ou utilisatrice en utilisant des
cookies
et persister les données dans une base de données.
Pas d’effets de bord dans les fonctions load
Pour la même raison, vos fonctions load
devraient être pures — donc ne comporter aucun effet de
bord (à l’exception peut-être d’un console.log(...)
occasionnel). Par exemple, vous pourriez être
tenté•e d’écrire dans un store ou un état global à l’intérieur d’une fonction load
afin de pouvoir
réutiliser la valeur dans vos composants :
import { const user: {
set: (value: any) => void;
}
user } from '$lib/user';
/** @type {import('./$types').PageLoad} */
export async function function load(event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>): MaybePromise<void | Record<string, any>>
load({ fetch: {
(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}
fetch
is equivalent to the native fetch
web API, with a few additional features:
- It can be used to make credentialed requests on the server, as it inherits the
cookie
and authorization
headers for the page request.
- It can make relative requests on the server (ordinarily,
fetch
requires a URL with an origin when used in a server context).
- Internal requests (e.g. for
+server.js
routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
- During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the
text
and json
methods of the Response
object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
- During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.
You can learn more about making credentialed requests with cookies here
fetch }) {
const const response: Response
response = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)
fetch('/api/user');
// NE FAITES JAMAIS ÇA !
const user: {
set: (value: any) => void;
}
user.set: (value: any) => void
set(await const response: Response
response.Body.json(): Promise<any>
json());
}
import { const user: {
set: (value: any) => void;
}
user } from '$lib/user';
import type { type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageLoad } from './$types';
export const const load: PageLoad
load: type PageLoad = (event: LoadEvent<Record<string, any>, Record<string, any> | null, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageLoad = async ({ fetch: {
(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}
fetch
is equivalent to the native fetch
web API, with a few additional features:
- It can be used to make credentialed requests on the server, as it inherits the
cookie
and authorization
headers for the page request.
- It can make relative requests on the server (ordinarily,
fetch
requires a URL with an origin when used in a server context).
- Internal requests (e.g. for
+server.js
routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
- During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the
text
and json
methods of the Response
object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
- During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.
You can learn more about making credentialed requests with cookies here
fetch }) => {
const const response: Response
response = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)
fetch('/api/user');
// NE FAITES JAMAIS ÇA !
const user: {
set: (value: any) => void;
}
user.set: (value: any) => void
set(await const response: Response
response.Body.json(): Promise<any>
json());
};
Comme avec l’exemple précédent, ceci enregistre les informations d’une personne dans un endroit qui est partagé avec tout le monde. Vous devriez plutôt simplement renvoyer les données...
/** @type {import('./$types').PageServerLoad} */
export async function function load({ fetch }: {
fetch: any;
}): Promise<{
user: any;
}>
load({ fetch: any
fetch }) {
const const response: any
response = await fetch: any
fetch('/api/user');
return {
user: any
user: await const response: any
response.json()
};
}
import type { type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad } from './$types';
export const const load: PageServerLoad
load: type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
type PageServerLoad = (event: Kit.ServerLoadEvent<Record<string, any>, Record<string, any>, string | null>) => MaybePromise<void | Record<string, any>>
PageServerLoad = async ({ fetch: {
(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
(input: string | URL | globalThis.Request, init?: RequestInit): Promise<Response>;
}
fetch
is equivalent to the native fetch
web API, with a few additional features:
- It can be used to make credentialed requests on the server, as it inherits the
cookie
and authorization
headers for the page request.
- It can make relative requests on the server (ordinarily,
fetch
requires a URL with an origin when used in a server context).
- Internal requests (e.g. for
+server.js
routes) go directly to the handler function when running on the server, without the overhead of an HTTP call.
- During server-side rendering, the response will be captured and inlined into the rendered HTML by hooking into the
text
and json
methods of the Response
object. Note that headers will not be serialized, unless explicitly included via filterSerializedResponseHeaders
- During hydration, the response will be read from the HTML, guaranteeing consistency and preventing an additional network request.
You can learn more about making credentialed requests with cookies here.
fetch }) => {
const const response: Response
response = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)
fetch('/api/user');
return {
user: any
user: await const response: Response
response.Body.json(): Promise<any>
json()
};
};
... et les passer aux composants qui en ont besoin, ou bien utiliser page.data
.
Si vous n’utilisez pas le SSR, il n’y a alors pas de risques d’exposer accidentellement les données
d’une personne à d’autres personnes. Mais vous devriez tout de même éviter les effets de bord dans
vos fonctions load
— le flux de données dans votre application sera bien plus facile à suivre.
Utiliser des états et des stores avec le contexte
Vous pourriez vous demander comment nous sommes capables d’utiliser page.data
et d’autres états
d’application (ou stores d’application) s’il ne faut pas utiliser d’état
global. La réponse est que les états et stores d’application sur le serveur utilisent l’API de
contexte de Svelte — l’état (ou le store) est attaché à l’arbre de
composant avec setContext
, et lorsque vous vous y abonnez, vous le lisez avec getContext
. Nous
pouvons faire la même chose avec nos propres états :
<script>
import { setContext } from 'svelte';
/** @type {import('./$types').LayoutProps} */
let { data } = $props();
// Passez une fonction référençant notre état
// au contexte pour que les composants enfant puissent y accéder
setContext('user', () => data.user);
</script>
<script lang="ts">
import { setContext } from 'svelte';
import type { LayoutProps } from './$types';
let { data }: LayoutProps = $props();
// Passez une fonction référençant notre état
// au contexte pour que les composants enfant puissent y accéder
setContext('user', () => data.user);
</script>
<script>
import { getContext } from 'svelte';
// Récupération du store user depuis le contexte
const user = getContext('user');
</script>
<p>Welcome {user().name}</p>
<script lang="ts">
import { getContext } from 'svelte';
// Récupération du store user depuis le contexte
const user = getContext('user');
</script>
<p>Welcome {user().name}</p>
Nous passons une fonction dans
setContext
pour garder la réactivité entre les frontières des composants. Apprenez-en plus ici
Legacy mode
Vous pouvez aussi utiliser des stores importés depuis
svelte/store
avec cette technique, mais si vous utilisez Svelte 5, il est plutôt recommandé d’utiliser la réactivité universelle.
Mettre à jour la valeur d’un état stocké en contexte dans des pages ou composants profonds lors du rendu côté de la page ne va pas affceter la valeur de cet état dans le composant parent car il aura déjà été rendu au moment où la valeur est mise à jour. A contrario, sur le client (lorsque le CSR est activé, ce qui est le cas par défaut), la valeur sera propagée, et les composants, pages, layouts situés plus haut dans la hiérarchie vont pouvoir réagir à cette nouvelle valeur. En conséquence, pour éviter des “flashs” liés à la mise à jour de valeur pendant l’hydratation, il est généralement recommandé de passer les états des parents vers les enfants plutôt que dans l’autre sens.
Si vous n’utilisez pas le SSR (et pouvez garantir que vous n’utiliserez pas le SSR dans le futur), vous pouvez alors conserver vos états dans un module partagé en toute sécurité, sans utiliser l’API de contexte.
L’état des pages et composants est préservé
Lorsque vous naviguez dans votre application, SvelteKit réutilise les composants de layout et de page déjà existants. Par exemple, si vous avez une route comme celle-ci...
<script>
/** @type {import('./$types').PageProps} */
let { data } = $props();
// CE CODE EST BUGUÉ !
const wordCount = data.content.split(' ').length;
const estimatedReadingTime = wordCount / 250;
</script>
<header>
<h1>{data.title}</h1>
<p>Temps de lecture : {Math.round(estimatedReadingTime)} minutes</p>
</header>
<div>{@html data.content}</div>
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
// CE CODE EST BUGUÉ !
const wordCount = data.content.split(' ').length;
const estimatedReadingTime = wordCount / 250;
</script>
<header>
<h1>{data.title}</h1>
<p>Temps de lecture : {Math.round(estimatedReadingTime)} minutes</p>
</header>
<div>{@html data.content}</div>
... alors naviguer de /blog/my-short-post
vers /blog/my-long-post
ne va pas déclencher la
destruction/re-création du layout, de la page et des autres composants. À la place, la prop data
(et par extension data.title
et data.content
) vont être mises à jour (comme cela est fait pour
tout autre composant Svelte) et, puisque le code n’est pas ré-exécuté, les méthodes de cycle de vie
comme onMount
et onDestroy
ne vont pas être rejouées et donc estimatedReadingTime
ne sera pas
recalculé.
Il faut donc plutôt rendre cette valeur réactive :
<script>
/** @type {import('./$types').PageProps} */
let { data } = $props();
let wordCount = $derived(data.content.split(' ').length);
let estimatedReadingTime = $derived(wordCount / 250);
</script>
<script lang="ts">
import type { PageProps } from './$types';
let { data }: PageProps = $props();
let wordCount = $derived(data.content.split(' ').length);
let estimatedReadingTime = $derived(wordCount / 250);
</script>
Si votre code dans
onMount
etonDestroy
a besoin d’être ré-exécuté après la navigation, vous pouvez plutôt utiliser afterNavigate et beforeNavigate respectivement.
Réutiliser des composants de cette manière signifie que l’état de choses comme la position des barres de défilement est préservé, et vous pouvez facilement animer les changements de valeur. Dans le cas où vous auriez besoin de complètement détruire et reconstruire un composant lors de navigation, vous pouvez utiliser cette méthode :
<script>
import { page } from '$app/state';
</script>
{#key page.url.pathname}
<BlogPost title={data.title} content={data.title} />
{/key}
Stocker de l’état dans l’URL
Si vous avez de l’état qui doit survivre à un rechargement de page et/ou affecter le rendu côté
serveur, comme des filtres ou des règles de tri sur un tableau, les paramètres de recherche d’URL
(comme ?sort=price&order=ascending
) sont un bon endroit pour le stocker. Vous pouvez le définir
dans les attributs de <a href="...">
ou <form action="...">
, ou le faire programmatiquement via
goto('?key=value')
. Ces valeurs d’état sont accessibles au sein des fonctions load
via le
paramètre url
, et au sein des composants via page.url.searchParams
.
Stocker les états éphémères dans des snapshots
Certaines états d’interface, comme “est-ce que cet accordéon est ouvert ?”, sont jetables — si l’utilisateur ou l’utilisatrice navigue sur un autre site ou recharge la page, ce n’est pas grave de perdre cet état. Dans certains cas, vous souhaitez persister cette information si la personne change de page et revient, mais la stocker dans l’URL ou dans une base de données serait trop d’effort pour l’enjeu. Pour ces cas-là, SvelteKit fournit des snapshots, qui vous permettent d’associer des états de composants à une entrée d’historique.
Modifier cette page sur Github llms.txt