12 KiB
| name | description | metadata | ||
|---|---|---|---|---|
| powersync-js-react | PowerSync React and Next.js integration — PowerSyncContext, useSuspenseQuery, useQuery, sync stream hooks, and Next.js/Vite setup |
|
PowerSync React & Next.js
Load this when building a React web app, Next.js app, or any Vite + React project. Load before package install for Vite projects — contains the required
vite.config.tssetup. Always loadpowersync-js.mdfirst.
Table of Contents
React-specific integration for the PowerSync JavaScript SDK. Use this reference alongside references/sdks/powersync-js.md when building React web apps, Next.js apps, or when using the React hooks from @powersync/react or @powersync/react-native.
| Resource | Description |
|---|---|
| React Integration Guide | Full React setup guide, consult for details beyond the inline examples. |
| Next.js Integration Guide | Full Next.js setup guide, consult for details beyond the inline examples. |
| React SDK API Reference | Full API reference for @powersync/react, consult only when the inline examples don't cover your case. |
Provider Setup
import { PowerSyncContext } from '@powersync/react'; // or @powersync/react-native
export function App() {
return (
<PowerSyncContext.Provider value={db}>
<YourApp />
</PowerSyncContext.Provider>
);
}
All hooks (useQuery, useSuspenseQuery, useStatus) read from this context via usePowerSync(). If the provider is missing, useQuery returns { isLoading: false, error: Error('PowerSync not configured.') }, and useSuspenseQuery throws.
useSuspenseQuery
useSuspenseQuery<RowType>(
query: string | CompilableQuery<RowType>,
parameters?: any[],
options?: { rowComparator?, reportFetching?, throttleMs?, runQueryOnce? }
): { data, isFetching } // no isLoading — always has data when it returns
Must be used inside a <Suspense> boundary. Throws an error boundary-catchable error if the query errors. Always pair with an <ErrorBoundary>:
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<Loading />}>
<DataComponent /> {/* calls useSuspenseQuery */}
</Suspense>
</ErrorBoundary>
useSuspenseQuery uses a QueryStore (one per PowerSyncDatabase instance, stored in a WeakMap). The store caches WatchedQuery instances keyed by:
"${sql} -- ${JSON.stringify(params)} -- ${JSON.stringify(options)}"
A query is evicted (closed) when all listeners are removed. useSuspenseQuery and useQuery with the same SQL/params/options share the same underlying WatchedQuery.
Sync Stream Hooks
React hooks for subscribing to named Sync Streams. Requires the PowerSync service to be configured with Sync Streams (edition 3 config). See Sync Streams Overview and Client-Side Usage for more information.
import { useSyncStream, useSuspenseSyncStream } from '@powersync/react';
// Non-suspense — returns null while subscription is being established
function ListsScreen() {
const streamStatus = useSyncStream({
name: 'lists',
parameters: { userId: currentUser.id },
priority: 1, // optional, 0-3
ttl: 3600, // optional, seconds to keep alive after unsubscribe
});
if (!streamStatus) return <Loading />; // subscription not yet ready
if (!streamStatus.subscription.hasSynced) return <Loading />;
return <ListsComponent />;
}
// Suspense version — never returns null, suspends instead
function ListsScreen() {
const streamStatus = useSuspenseSyncStream({
name: 'lists',
parameters: { userId: currentUser.id },
});
// streamStatus.subscription.hasSynced is guaranteed true here
return <ListsComponent />;
}
Automatic cleanup: Both hooks unsubscribe when the component unmounts. The TTL keeps data active for the specified duration after unsubscribe.
SyncStreamStatus Fields
interface SyncStreamStatus {
subscription: {
name: string;
parameters: Record<string, any> | null;
active: boolean; // currently receiving data
isDefault: boolean; // was configured as a default stream on the server
hasExplicitSubscription: boolean;
expiresAt: Date | null; // when TTL expires
hasSynced: boolean; // has completed at least one full sync
lastSyncedAt: Date | null;
};
progress: {
downloadedFraction: number; // 0.0-1.0
downloadedOperations: number;
totalOperations: number;
} | null;
priority: number | null;
}
Next.js Setup
PowerSync is tailored for client-side applications. Next.js evaluates code in a Node.js runtime during SSR — the PowerSync Web SDK requires browser APIs not available in Node.js. It performs no-ops in Node.js rather than throwing errors, but no data is available during SSR. Always isolate PowerSync to client-side code.
Install
npm install @powersync/web@latest @journeyapps/wa-sqlite@latest @powersync/react@latest
Copy Worker Assets (Turbopack)
Turbopack doesn't support dynamic imports of workers yet. Add a postinstall script to copy pre-bundled worker files to your public directory:
{
"scripts": {
"postinstall": "powersync-web copy-assets -o public"
}
}
Run it once after install, then add to .gitignore:
public/@powersync/*
next.config.ts — Turbopack (Next.js 16+)
module.exports = {
images: {
disableStaticImages: true
},
turbopack: {}
};
next.config.ts — Webpack (legacy, pre-Next.js 16)
module.exports = {
webpack: (config, { isServer }) => {
config.experiments = {
...config.experiments,
asyncWebAssembly: true,
topLevelAwait: true,
};
if (!isServer) {
config.module.rules.push({
test: /\.wasm$/,
type: 'asset/resource',
});
}
return config;
}
};
SystemProvider
Create a client-only provider component. The disableSSRWarning: true flag suppresses the Node.js no-op warning. The pre-bundled worker paths reference the files copied by powersync-web copy-assets.
// components/providers/SystemProvider.tsx
'use client';
import { AppSchema } from '@/lib/powersync/AppSchema';
import { BackendConnector } from '@/lib/powersync/BackendConnector';
import { PowerSyncContext } from '@powersync/react';
import { PowerSyncDatabase, WASQLiteOpenFactory, createBaseLogger, LogLevel } from '@powersync/web';
import React, { Suspense } from 'react';
const factory = new WASQLiteOpenFactory({
dbFilename: 'powersync.db',
// Pre-bundled worker — required for Turbopack
worker: '/@powersync/worker/WASQLiteDB.umd.js'
});
export const db = new PowerSyncDatabase({
database: factory,
schema: AppSchema,
flags: {
disableSSRWarning: true
},
sync: {
worker: '/@powersync/worker/SharedSyncImplementation.umd.js'
}
});
const connector = new BackendConnector();
db.connect(connector);
export const SystemProvider = ({ children }: { children: React.ReactNode }) => {
return (
<Suspense>
<PowerSyncContext.Provider value={db}>{children}</PowerSyncContext.Provider>
</Suspense>
);
};
Update layout.tsx
// app/layout.tsx
'use client';
import { SystemProvider } from '@/app/components/providers/SystemProvider';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<SystemProvider>{children}</SystemProvider>
</body>
</html>
);
}
Using PowerSync in Pages
// app/page.tsx
'use client';
import { useQuery, useStatus, usePowerSync } from '@powersync/react';
export default function Page() {
const powersync = usePowerSync();
const status = useStatus();
const { data: lists, isLoading } = useQuery('SELECT * FROM lists');
return (
<ul>
{lists.map(list => <li key={list.id}>{list.name}</li>)}
</ul>
);
}
All components that use PowerSync hooks must have 'use client' at the top. PowerSync hooks are not compatible with React Server Components.
Vite Setup
Vite requires vite-plugin-wasm + vite-plugin-top-level-await to handle the WASM SQLite engine.
npm install -D vite-plugin-wasm vite-plugin-top-level-await
// vite.config.ts
import wasm from 'vite-plugin-wasm'
import topLevelAwait from 'vite-plugin-top-level-await'
export default defineConfig({
plugins: [react(), wasm(), topLevelAwait()],
optimizeDeps: {
exclude: ['@journeyapps/wa-sqlite', '@powersync/web'],
},
worker: {
format: 'es',
plugins: () => [wasm(), topLevelAwait()],
},
})
Do NOT add
optimizeDeps: { include: ['@powersync/web > js-logger'] }— this dependency path does not exist in current SDK versions and causes build warnings. Theexcludeconfiguration above is sufficient.
Common Pitfalls
React Strict Mode destroys PowerSyncDatabase in useEffect
In development, React Strict Mode unmounts and remounts every component. If you create a PowerSyncDatabase inside a useEffect cleanup/setup cycle, the first mount's cleanup releases the shared-worker DB proxy before the second mount can use it — the database connection silently breaks.
// WRONG — Strict Mode will destroy this on the dev double-mount
function App() {
const [db, setDb] = useState<PowerSyncDatabase | null>(null);
useEffect(() => {
const database = new PowerSyncDatabase({ schema, database: { dbFilename: 'app.db' } });
database.connect(connector);
setDb(database);
return () => { database.close(); }; // Kills the DB on Strict Mode re-mount
}, []);
// ...
}
// CORRECT — create once at module scope (or use a stable singleton)
const db = new PowerSyncDatabase({ schema, database: { dbFilename: 'app.db' } });
db.connect(connector);
function App() {
return (
<PowerSyncContext.Provider value={db}>
<YourApp />
</PowerSyncContext.Provider>
);
}
Keep the DB instance stable across transient remounts. Only call db.close() when the app is truly done with it (e.g. on logout with disconnectAndClear()). Disabling enableMultiTabs can mask the symptom temporarily but does not fix the root cause.
Suspense requires ErrorBoundary
useSuspenseQuery throws query errors upward — they go to the nearest <ErrorBoundary>, not <Suspense>. Without an ErrorBoundary, query errors crash the component tree silently.
<ErrorBoundary fallback={<ErrorUI />}>
<Suspense fallback={<Loading />}>
<DataComponent />
</Suspense>
</ErrorBoundary>
Next.js: forgetting 'use client'
Any component that calls usePowerSync, useQuery, useStatus, or useSuspenseQuery must be a Client Component. Forgetting 'use client' causes a build error or a runtime error about hooks in Server Components.
Next.js: awaiting db.connect() at module scope
connect() is fire-and-forget. Calling await db.connect(connector) at module scope in a Next.js file will block the module evaluation. Call db.connect(connector) without await in your provider, then use db.waitForFirstSync() if you need to gate rendering on data availability.