EnglishBac à sable

Stockage dans une application

@fluixi/core/storage relie @fluixi/storage à l'application : il choisit le support selon l'endroit où le code s'exécute, garde l'état du serveur par requête, et lit la même valeur dans le rendu serveur et dans le navigateur.

const prefs = $storage({ scope: 'cookie', namespace: 'ui' });
const theme = $persisted(prefs, 'theme', { fallback: 'light' });

<button onClick={() => theme.set((t) => (t === 'light' ? 'dark' : 'light'))}>
  {() => theme()}
</button>

Le serveur rend le thème choisi par le visiteur, depuis son cookie, et le navigateur part de la même valeur : pas de flash, pas d'écart à l'hydratation. Dans du code de paquet, les mêmes appels s'écrivent useStorage et persisted, importés de @fluixi/core/storage.

Portées

useStorage(options) prend les options de createStorage (espace de noms, migrations, sérialiseur) et un scope :

Portée Serveur Navigateur
auto (par défaut) le stockage propre à la requête localStorage
cookie les cookies de la requête, renvoyés en Set-Cookie document.cookie
request des valeurs pour cette requête seulement des valeurs pour cette page
local, session lève une erreur : ce stockage n'existe pas localStorage, sessionStorage

Prenez cookie pour tout ce sur quoi le rendu serveur doit être d'accord : un thème, une langue, un bandeau fermé. Prenez local pour ce qui ne concerne que le navigateur. auto prend d'abord le stockage d'un <StorageProvider>, s'il y en a un.

Sur le serveur, l'état ne vit jamais dans un module. Chaque requête a ses propres adaptateurs : deux visiteurs rendus en même temps ne voient jamais les valeurs l'un de l'autre. Appelé sur le serveur hors de toute requête (au niveau d'un module, dans un script), useStorage lève une erreur plutôt que de partager en silence un même stockage entre tous.

Cookies

const prefs = useStorage({
  scope: 'cookie',
  namespace: 'ui',
  cookie: { maxAge: 60 * 60 * 24 * 365, sameSite: 'lax' },
});

httpOnly vaut false par défaut ici, puisque la moitié navigateur doit lire le cookie. Mettez-le à true pour une valeur que seul le serveur lit. Les autres attributs gardent leurs valeurs sûres : Secure, SameSite=Lax, Path=/.

Un cookie écrit pendant le rendu arrive dans la réponse, pages en streaming comprises, tant que le premier morceau du corps n'est pas parti. Ensuite les en-têtes sont envoyés et l'écriture lève une erreur qui le dit. Une valeur décidée tard a sa place dans un middleware, les données d'une route ou une fonction serveur.

persisted

persisted(storage, key, options) est un signal dont la valeur vit dans une clé de stockage. Il part de la valeur stockée, écrit au set, et suit les changements faits par tout autre stockage sur le même adaptateur, et par les autres onglets quand le support les signale.

const sidebar = persisted(useStorage({ namespace: 'ui' }), 'sidebar', { fallback: true });

sidebar();                // true tant que rien ne change
sidebar.set((open) => !open);
sidebar.remove();         // retour à la valeur par défaut

Options : fallback (la valeur tant que la clé est absente ; null sans elle), ttl pour chaque écriture, name pour les devtools. Une écriture refusée par le stockage lève une erreur et laisse le signal tel quel.

Sur un stockage asynchrone ou chiffré, persisted garde la valeur par défaut jusqu'à avoir lu la valeur stockée (await signal.ready), met le signal à jour tout de suite au set, et revient en arrière si l'écriture échoue. Cette lecture ne peut pas se terminer pendant le rendu serveur : gardez ces valeurs hors de ce que le serveur rend.

Ne recopiez pas un signal dans un stockage avec un effet ; persisted est le seul endroit où les deux vivent.

Stockage asynchrone et chiffré

useAsyncStorage a les mêmes portées, chaque opération attendue, et accepte encryption :

const vault = useAsyncStorage<{ token: string }>({
  namespace: 'auth',
  encryption: { keyProvider },
});
await vault.set('token', token, { encrypt: true });

Voir Chiffrement pour ce qu'il protège et ce qu'il ne protège pas.

Fournir un stockage

<StorageProvider storage={createStorage({ adapter: memoryStorage() })}>
  <Settings />
</StorageProvider>

Chaque useStorage() en dessous, avec la portée auto, reçoit ce stockage : la mémoire d'un test, un stockage propre à l'application. Un stockage asynchrone va de la même façon à useAsyncStorage().

Transmettre des valeurs au navigateur

Une valeur propre à la requête, calculée sur le serveur, a disparu quand le navigateur prend la main. hydrateStorage envoie les clés choisies avec la page :

const scratch = useStorage<{ draft: Draft }>({ scope: 'request', namespace: 'editor' });
hydrateStorage(scratch, ['draft']);

Appelez-le avec les mêmes clés des deux côtés. Sur le serveur, il place les valeurs, avec leur expiration et leur version, dans les données de la page ; dans le navigateur, le premier appel les écrit dans le stockage, et les suivants laissent la valeur du navigateur intacte. Les données de la page sont lisibles par quiconque voit la page : n'y mettez jamais de secret. Une valeur chiffrée lève une erreur ici plutôt que de voyager en clair.

Le thème

Un thème est la valeur la plus courante sur laquelle les deux côtés doivent s'accorder. createColorScheme, createTheme et leurs providers le gardent dans un cookie par ce stockage, envoient l'attribut avec la page et appliquent un choix stocké avant l'affichage. Voir Thèmes.

Intrinsèques

Dans une application construite par @fluixi/start, ceux-ci n'ont pas besoin d'import :

Intrinsèque Correspond à
$storage(options) useStorage
$asyncStorage(options) useAsyncStorage
$persisted(storage, key, options) persisted, lu comme un signal