SSR — Next.js & Nuxt
Syncraft Labs is a client-side library — it depends on IndexedDB and BroadcastChannel, which don’t exist on the server. This guide covers how to integrate safely with server-rendering frameworks.
Why SSR Needs Special Handling
Without the Provider/Plugin pattern, stores would be module-level singletons — shared across all requests on the server. This causes:
| Problem | Impact |
|---|---|
| Data leak between users | User A sees User B’s state |
| Memory growth | Stores never cleaned up between requests |
| Hydration mismatch | Server renders undefined, client hydrates from IndexedDB |
Syncraft Labs solves this with request-scoped registries:
- React:
<SyncraftProvider>creates a newMapper render tree - Vue:
createSyncraft()creates a newMapper app instance
Next.js (App Router)
1. Create a Client Provider
IndexedDB does not exist on the server. Mark the provider as a Client Component:
"use client";
import { SyncraftProvider } from "@syncraft-labs/react";
export function Providers({ children }: { children: React.ReactNode }) { return <SyncraftProvider>{children}</SyncraftProvider>;}2. Wrap in Root Layout
import { Providers } from "./providers";
export default function RootLayout({ children,}: { children: React.ReactNode;}) { return ( <html lang="en"> <body> <Providers>{children}</Providers> </body> </html> );}3. Use useSync Only in Client Components
"use client";
import { useSync } from "@syncraft-labs/react";
interface TodoState { todos: Array<{ id: string; text: string; done: boolean }>;}
export function TodoList() { const { data, update, isHydrating } = useSync<TodoState>("todos", { initialState: { todos: [] }, fetcher: () => fetch("/api/todos").then((r) => r.json()), });
if (isHydrating) return <p>Loading…</p>;
return ( <ul> {data?.todos.map((t) => ( <li key={t.id}>{t.text}</li> ))} </ul> );}4. Use in a Server Component Page
// app/page.tsx (Server Component — no "use client")import { TodoList } from "@/components/todo-list";
export default function HomePage() { return ( <main> <h1>My Todos</h1> {/* TodoList is a Client Component, safe to use here */} <TodoList /> </main> );}Handling Hydration Mismatch
During SSR, useSync returns data: undefined (IndexedDB isn’t available). On the client, it hydrates and returns the persisted state. This is handled gracefully by isHydrating:
// ✅ No mismatch — server and client both show the loading stateif (isHydrating) return <Skeleton />;
// After hydration, client renders from IndexedDBreturn <DataView data={data} />;Tip: If using React Suspense with
useSyncSuspense, wrap the component in<Suspense>. The thrown promise during hydration integrates naturally with Suspense boundaries.
Next.js (Pages Router)
import { SyncraftProvider } from "@syncraft-labs/react";import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) { return ( <SyncraftProvider> <Component {...pageProps} /> </SyncraftProvider> );}All pages and components within _app can safely use useSync.
Nuxt 3
1. Create a Client-Only Plugin
import { createSyncraft } from "@syncraft-labs/vue";
export default defineNuxtPlugin((nuxtApp) => { const syncraft = createSyncraft(); nuxtApp.vueApp.use(syncraft);});Note: The
.client.tssuffix ensures this plugin only runs in the browser. Nuxt will skip it during SSR.
2. Use the Composable in Components
<script setup lang="ts">import { useSync } from "@syncraft-labs/vue";
interface TodoState { todos: Array<{ id: string; text: string; done: boolean }>;}
const { data, update, isHydrating } = useSync<TodoState>("todos", { initialState: { todos: [] }, fetcher: () => $fetch("/api/todos"),});</script>
<template> <p v-if="isHydrating">Loading…</p> <ul v-else> <li v-for="t in data?.todos" :key="t.id">{{ t.text }}</li> </ul></template>3. Client-Only Components (Optional)
If you want to prevent any SSR rendering for Syncraft-powered components, use Nuxt’s <ClientOnly>:
<template> <div> <h1>My Todos</h1> <ClientOnly> <TodoList /> <template #fallback> <p>Loading…</p> </template> </ClientOnly> </div></template>How Isolation Works
Request 1 (User A) Request 2 (User B)┌─────────────────────┐ ┌─────────────────────┐│ SyncraftProvider │ │ SyncraftProvider ││ registry = new Map │ │ registry = new Map ││ ┌────────────────┐ │ │ ┌────────────────┐ ││ │ "todos" → StoreA│ │ │ │ "todos" → StoreB│ ││ └────────────────┘ │ │ └────────────────┘ │└─────────────────────┘ └─────────────────────┘ ↕ Isolated ↕ IsolatedEach SyncraftProvider (React) or createSyncraft() (Vue) creates a fresh Map registry. Stores are scoped to that registry — no data leaks between requests.