Skip to main content

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 :

+page.server
let let user: anyuser;

/** @type {import('./$types').PageServerLoad} */
export function 
function load(): {
    user: any;
}
@type{import('./$types').PageServerLoad}
load
() {
return { user: anyuser }; } /** @satisfies {import('./$types').Actions} */ export const
const actions: {
    default: ({ request }: {
        request: any;
    }) => Promise<void>;
}
@satisfies{import('./$types').Actions}
actions
= {
default: ({ request }: {
    request: any;
}) => Promise<void>
default
: async ({ request: anyrequest }) => {
const const data: anydata = await request: anyrequest.formData(); // NE FAITES JAMAIS ÇA ! let user: anyuser = { name: anyname: const data: anydata.get('name'), embarrassingSecret: anyembarrassingSecret: const data: anydata.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: anyuser; export const const load: PageServerLoadload:
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: anyuser }; }; 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: FormDatadata = await request: Request

The original request object.

request
.Body.formData(): Promise<FormData>formData();
// NE FAITES JAMAIS ÇA ! let user: anyuser = { name: FormDataEntryValue | nullname: const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullget('name'), embarrassingSecret: FormDataEntryValue | nullembarrassingSecret: const data: FormDatadata.FormData.get(name: string): FormDataEntryValue | nullget('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 :

+page
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>>
@type{import('./$types').PageLoad}
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: Responseresponse = 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) => voidset(await const response: Responseresponse.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: PageLoadload: 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: Responseresponse = 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) => voidset(await const response: Responseresponse.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...

+page
/** @type {import('./$types').PageServerLoad} */
export async function 
function load({ fetch }: {
    fetch: any;
}): Promise<{
    user: any;
}>
@type{import('./$types').PageServerLoad}
load
({ fetch: anyfetch }) {
const const response: anyresponse = await fetch: anyfetch('/api/user'); return { user: anyuser: await const response: anyresponse.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: PageServerLoadload:
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: Responseresponse = await fetch: (input: string | URL | globalThis.Request, init?: RequestInit) => Promise<Response> (+1 overload)fetch('/api/user'); return { user: anyuser: await const response: Responseresponse.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 :

src/routes/+layout
<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>
src/routes/user/+page
<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...

src/routes/blog/[slug]/+page
<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 :

src/routes/blog/[slug]/+page
<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 et onDestroy 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