SSR Hydration
SsrHydrator reads the server-rendered cache snapshot from window.__QORA_STATE__ and injects it into QoraClient before the first frame renders.
SsrHydrator is only available on Flutter Web. On native and server targets the import resolves to a no-op stub, so the same code compiles everywhere without #if guards.
How It Works
- Your server prefetches data and embeds it as a JSON object in a
<script>tag beforemain.dart.jsloads. SsrHydrator.hydrate()readswindow.__QORA_STATE__, validates the payload, and callsclient.queueHydration()for each entry.- On the first
fetchQuery<T>orwatchQuery<T>call for a key, the queued value is cast toTand injected as aSuccessstate. - If
updatedAtMsis omitted or the data is older thanstaleTime, Qora automatically triggers a background revalidation (SWR).
Server Setup
Embed the prefetched data in a <script> tag before main.dart.js:
<script>
window.__QORA_STATE__ = {
'["users",1]': {
"data": { "id": 1, "name": "Ada", "email": "ada@example.com" },
"updatedAtMs": 1700000000000
},
'["posts"]': {
"data": [
{ "id": 10, "title": "Hello World" },
{ "id": 11, "title": "Flutter Web" }
]
}
};
</script>
<script src="main.dart.js" defer></script>
Keys are JSON-encoded normalised query keys: the same format Qora uses internally (e.g. '["users",1]' for key ['users', 1]).
updatedAtMs is optional. When omitted, Qora treats the data as epoch-time-old (always stale): it is served immediately but a background revalidation fires at the first mount.
Flutter Setup
Call SsrHydrator.hydrate() before runApp:
import 'package:flutter/material.dart';
import 'package:qora/qora.dart';
import 'package:qora_flutter/qora_flutter.dart';
void main() {
final client = QoraClient();
// Hydrate from window.__QORA_STATE__ (no-op on 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()));
}
Keys in window.__QORA_STATE__ that are absent from deserializers are silently skipped. If deserializers is omitted entirely, raw dynamic values from dartify() are queued directly: suitable when your data is already a plain Map/List.
Security
SsrHydrator is XSS-safe by design:
- Uses
dartify()for JS → Dart conversion: produces plain maps/lists/strings, never HTML. NoinnerHTMLoreval. - Validates the top-level type (
Map) and each entry (Mapwith a'data'key) before processing. - Wraps every deserializer call in
try/catch: a corrupt entry cannot crash the app.
Your responsibility: JSON-encode user-controlled data on the server before embedding it in __QORA_STATE__. Raw HTML interpolation bypasses Qora's protections and enables XSS.
Freshness and SWR
The updatedAtMs timestamp is stored as the updatedAt on the injected Success state. Qora's stale-while-revalidate logic uses it to decide whether to trigger a background fetch:
// staleTime: 5 minutes
// updatedAtMs: 4 minutes ago → fresh, no background fetch
// updatedAtMs: 6 minutes ago → stale, SWR fires immediately on mount
// updatedAtMs: absent → always stale
client.watchQuery<User>(
key: ['users', 1],
fetcher: () => api.getUser(1),
options: const QoraOptions(staleTime: Duration(minutes: 5)),
);
SsrHydrator API
Constructor
const SsrHydrator(QoraClient client)
hydrate
void hydrate({Map<String, dynamic Function(dynamic)>? deserializers})
Read window.__QORA_STATE__ and enqueue hydration entries.
| Parameter | Type | Description |
|---|---|---|
deserializers | Map<String, dynamic Function(dynamic)>? | Map from string-encoded key to a function converting raw JSON to typed Dart. Keys absent from this map are skipped. Pass null to queue raw values. |
With PersistQoraClient
SsrHydrator and PersistQoraClient both use queueHydration() under the hood. They compose without conflict: the last write for a given key wins (first typed access picks it up):
final client = PersistQoraClient(storage: storage);
client.registerSerializer<User>(...);
await storage.init();
await client.hydrate(); // from disk
SsrHydrator(client).hydrate(...); // from server (overwrites disk entry if key matches)
runApp(QoraScope(client: client, child: const MyApp()));
Related
- Persistence Guide: Disk-based cache with
PersistQoraClient - QoraClient API:
queueHydration,hydrateQuery - Quick Start