Guides Pratiques

Hydratation SSR

Éliminer le flash de chargement sur les pages Flutter Web rendues côté serveur en injectant le cache serveur dans QoraClient via SsrHydrator.

SsrHydrator lit le snapshot du cache rendu côté serveur depuis window.__QORA_STATE__ et l'injecte dans QoraClient avant le rendu de la première frame.

SsrHydrator est uniquement disponible sur Flutter Web. Sur les cibles natives et serveur, l'import se résout en stub sans opération, de sorte que le même code compile partout sans gardes #if.


Fonctionnement

  1. Le serveur précharge les données et les intègre sous forme d'objet JSON dans une balise <script> avant le chargement de main.dart.js.
  2. SsrHydrator.hydrate() lit window.__QORA_STATE__, valide le payload et appelle client.queueHydration() pour chaque entrée.
  3. Au premier appel fetchQuery<T> ou watchQuery<T> pour une clé, la valeur en file est castée en T et injectée comme état Success.
  4. Si updatedAtMs est omis ou si les données sont plus anciennes que staleTime, Qora déclenche automatiquement une revalidation en arrière-plan (SWR).

Configuration serveur

Intégrer les données préchargées dans une balise <script> avant main.dart.js :

index.html
<script>
  window.__QORA_STATE__ = {
    '["users",1]': {
      "data": { "id": 1, "name": "Ada", "email": "ada@example.com" },
      "updatedAtMs": 1700000000000
    },
    '["posts"]': {
      "data": [
        { "id": 10, "title": "Bonjour le monde" },
        { "id": 11, "title": "Flutter Web" }
      ]
    }
  };
</script>
<script src="main.dart.js" defer></script>

Les clés sont des clés de requête normalisées encodées en JSON : le même format qu'utilise Qora en interne (ex. '["users",1]' pour la clé ['users', 1]).

updatedAtMs est optionnel. Quand omis, Qora traite les données comme datant de l'epoch (toujours périmées) : elles sont servies immédiatement mais une revalidation en arrière-plan se déclenche au premier montage.


Configuration Flutter

Appeler SsrHydrator.hydrate() avant runApp :

main.dart
import 'package:flutter/material.dart';
import 'package:qora/qora.dart';
import 'package:qora_flutter/qora_flutter.dart';

void main() {
  final client = QoraClient();

  // Hydrater depuis window.__QORA_STATE__ (sans opération sur non-web)
  SsrHydrator(client).hydrate(
    deserializers: {
      '["users",1]': (json) => User.fromJson(json as Map<String, dynamic>),
      '["posts"]':   (json) => (json as List).map(Post.fromJson).toList(),
    },
  );

  runApp(QoraScope(client: client, child: const MyApp()));
}

Les clés de window.__QORA_STATE__ absentes de deserializers sont silencieusement ignorées. Si deserializers est entièrement omis, les valeurs dynamic brutes de dartify() sont mises en file directement : adapté quand les données sont déjà des Map/List simples.


Sécurité

SsrHydrator est sûr contre les XSS par conception :

  • Utilise dartify() pour la conversion JS → Dart : produit des maps/listes/chaînes simples, jamais de HTML. Pas d'innerHTML ni d'eval.
  • Valide le type de niveau supérieur (Map) et chaque entrée (Map avec une clé 'data') avant traitement.
  • Encapsule chaque appel au désérialiseur dans un try/catch : une entrée corrompue ne peut pas planter l'application.

Responsabilité : encoder en JSON les données contrôlées par l'utilisateur côté serveur avant de les intégrer dans __QORA_STATE__. L'interpolation HTML brute contourne les protections de Qora et permet des XSS.


Fraîcheur et SWR

L'horodatage updatedAtMs est stocké comme updatedAt sur l'état Success injecté. La logique stale-while-revalidate de Qora l'utilise pour décider de déclencher un fetch en arrière-plan :

main.dart
// staleTime: 5 minutes
// updatedAtMs: il y a 4 minutes → frais, pas de fetch en arrière-plan
// updatedAtMs: il y a 6 minutes → périmé, SWR se déclenche immédiatement au montage
// updatedAtMs: absent           → toujours périmé
client.watchQuery<User>(
  key: ['users', 1],
  fetcher: () => api.getUser(1),
  options: const QoraOptions(staleTime: Duration(minutes: 5)),
);

API SsrHydrator

Constructeur

const SsrHydrator(QoraClient client)

hydrate

void hydrate({Map<String, dynamic Function(dynamic)>? deserializers})

Lire window.__QORA_STATE__ et mettre en file les entrées d'hydratation.

ParamètreTypeDescription
deserializersMap<String, dynamic Function(dynamic)>?Map de clé encodée en chaîne vers une fonction convertissant le JSON brut en Dart typé. Les clés absentes de cette map sont ignorées. Passer null pour mettre en file les valeurs brutes.

Avec PersistQoraClient

SsrHydrator et PersistQoraClient utilisent tous deux queueHydration() en interne. Ils se composent sans conflit : la dernière écriture pour une clé donnée gagne (le premier accès typé la récupère) :

main.dart
final client = PersistQoraClient(storage: storage);
client.registerSerializer<User>(...);

await storage.init();
await client.hydrate();           // depuis le disque
SsrHydrator(client).hydrate(...); // depuis le serveur (écrase l'entrée disque si la clé correspond)

runApp(QoraScope(client: client, child: const MyApp()));

Voir aussi

Copyright © 2026