SSR
netron-react supports server-side rendering by serialising the QueryCache on the server and rehydrating on the client.
Pattern — Next.js / Remix style
// server-side route loader:
export async function loader(req) {
const client = new NetronReactClient({
url: process.env.API_URL,
transport: 'http',
ssr: { enabled: true },
});
await client.connect();
// Prefetch everything the page needs:
const users = client.service<UserService>('users');
const projects = client.service<ProjectService>('projects');
await client.prefetchQuery(['users', 'list', { filter: 'active' }], () => users.list.call({ filter: 'active' }));
await client.prefetchQuery(['projects', 'getMine'], () => projects.getMine.call());
const dehydratedState = client.dehydrate();
return { dehydratedState };
}
// client-side root:
function App({ dehydratedState }: { dehydratedState: DehydratedState }) {
const [client] = useState(() => {
const c = new NetronReactClient({
url: new URL('/api', window.location.origin).href,
transport: 'http',
});
c.hydrate(dehydratedState); // populate cache from server
return c;
});
return (
<NetronProvider client={client}>
<Outlet />
</NetronProvider>
);
}The server's dehydrate() returns a plain-object snapshot of
the cache (synchronously — no await). The client's hydrate()
method populates the fresh client's cache from that snapshot.
Hydration semantics
After hydration:
Cached queries from the server don't refetch on mount (until they go stale per
staleTime).Subscriptions don't carry over — they re-subscribe on the client side.
Mutation state isn't dehydrated (it's request-local).
useQuery(['users','list', { filter: 'active' }], {
staleTime: 30_000,
// Server-fetched + dehydrated; client mount won't refetch unless > 30s stale.
});useHydration
When client-side rendering needs to branch on whether hydration has completed:
import { useHydration } from '@omnitron-dev/netron-react';
function ColorMode() {
const isHydrated = useHydration();
// Avoid a hydration mismatch by deferring browser-only state entirely.
// Reading it above this line would run it on the server too, where
// `window` does not exist.
if (!isHydrated) return null;
const dark = window.matchMedia('(prefers-color-scheme: dark)').matches;
return <Icon name={dark ? 'sun' : 'moon'} />;
}Useful for any UI that depends on browser-only state (localStorage,
window.matchMedia, etc.) — render null until hydrated to
avoid SSR/client divergence.
Streaming SSR
// Next.js 13+ / React 19 server components:
import { renderToReadableStream } from 'react-dom/server';
const stream = await renderToReadableStream(<App />, {
onError: console.error,
});For streaming, prefer <Suspense> boundaries around data-bound
sections; pass { suspense: true } to useQuery and it
integrates cleanly.
Per-user dehydration
Use a request-scoped cache for authenticated data. For an upstream service that accepts bearer credentials, put the token on the HTTP client that makes the call. Keep the upstream base URL in trusted server configuration.
import { HttpClient } from '@omnitron-dev/netron-browser';
import { NetronReactClient } from '@omnitron-dev/netron-react';
async function prefetchWorkspace(apiUrl: string, accessToken: string) {
const rpc = new HttpClient({
url: apiUrl,
redirect: 'error',
headers: { Authorization: `Bearer ${accessToken}` },
});
const client = new NetronReactClient({ url: apiUrl, transport: 'http' });
await rpc.connect();
try {
await client.prefetchQuery(['workspace', 'current'], () =>
rpc.invoke('Workspace', 'current', []),
);
return client.dehydrate();
} finally {
await rpc.disconnect();
}
}Only include data the requesting user may receive in the page.
Excluding queries from dehydration
dehydrate() takes no arguments — it automatically dehydrates
only successful queries. Errored / in-flight queries are
skipped so the client retries fresh, and mutation state is
request-local. To keep sensitive entries out of the snapshot,
remove them from the cache before dehydrating:
// Drop sensitive queries before serialising:
client.removeQueries({ queryKey: ['admin'] });
client.removeQueries({ queryKey: ['tokens'] });
const dehydratedState = client.dehydrate();Bundle size considerations
The browser and SSR use the same package entry point. Selecting
transport: 'http' runs the HTTP path and avoids opening a socket on the server.
Bundle size depends on the imports and bundler output.
Caveats
Subscriptions don't persist. Server-side they aren't initiated; client takes over.
Optimistic mutations in flight at dehydration time are lost — let them complete first.
Time-sensitive data (clocks, "X minutes ago") needs
suppressHydrationWarningon the wrapper element oruseHydration-gated rendering.
Best practices
Prefetch above-the-fold data. Don't dehydrate the whole app — pick the queries that fill the initial render.
Per-user clients. Don't reuse a server-side client across requests.
staleTime > 0for dehydrated queries. Otherwise the client refetches immediately, defeating SSR.useHydrationfor browser-only UI. Theme toggle, geolocation, feature flags — gate them.
Anti-patterns
Sharing a dehydrated cache across users. Leaks data.
Streaming WebSocket on the server. WS doesn't work pre-hydration; the cache rehydrates, but live subscriptions start on the client.
client.hydrate()without a matching server prefetch. The cache is empty; you've added complexity for nothing.
See also
Caching —
dehydrate/prefetchQuerynetron-react —
useQuerySSR semanticsTransports — HTTP for edge runtimes