--- name: powersync-js-react description: PowerSync React and Next.js integration — PowerSyncContext, useSuspenseQuery, useQuery, sync stream hooks, and Next.js/Vite setup metadata: tags: react, nextjs, vite, hooks, useSuspenseQuery, useQuery, PowerSyncContext, javascript, typescript --- # 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.ts` setup. Always load `powersync-js.md` first. ## Table of Contents - [Provider Setup](#provider-setup) - [useSuspenseQuery](#usesuspensequery) - [Sync Stream Hooks](#sync-stream-hooks) - [Next.js Setup](#nextjs-setup) - [Vite Setup](#vite-setup) - [Common Pitfalls](#common-pitfalls) 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](https://docs.powersync.com/client-sdks/frameworks/react.md) | Full React setup guide, consult for details beyond the inline examples. | | [Next.js Integration Guide](https://docs.powersync.com/client-sdks/frameworks/next-js.md) | Full Next.js setup guide, consult for details beyond the inline examples. | | [React SDK API Reference](https://powersync-ja.github.io/powersync-js/react-sdk) | Full API reference for `@powersync/react`, consult only when the inline examples don't cover your case. | ## Provider Setup ```tsx import { PowerSyncContext } from '@powersync/react'; // or @powersync/react-native export function App() { return ( ); } ``` 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 ```ts useSuspenseQuery( query: string | CompilableQuery, parameters?: any[], options?: { rowComparator?, reportFetching?, throttleMs?, runQueryOnce? } ): { data, isFetching } // no isLoading — always has data when it returns ``` Must be used inside a `` boundary. Throws an error boundary-catchable error if the query errors. Always pair with an ``: ```tsx }> }> {/* calls useSuspenseQuery */} ``` `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](https://docs.powersync.com/sync/streams/overview.md) and [Client-Side Usage](https://docs.powersync.com/sync/streams/client-usage.md#react-hooks) for more information. ```tsx 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 ; // subscription not yet ready if (!streamStatus.subscription.hasSynced) return ; return ; } // 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 ; } ``` Automatic cleanup: Both hooks unsubscribe when the component unmounts. The TTL keeps data active for the specified duration after unsubscribe. ### SyncStreamStatus Fields ```ts interface SyncStreamStatus { subscription: { name: string; parameters: Record | 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 ```bash 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: ```json { "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+) ```typescript module.exports = { images: { disableStaticImages: true }, turbopack: {} }; ``` ### `next.config.ts` — Webpack (legacy, pre-Next.js 16) ```typescript 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`. ```tsx // 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 ( {children} ); }; ``` ### Update `layout.tsx` ```tsx // app/layout.tsx 'use client'; import { SystemProvider } from '@/app/components/providers/SystemProvider'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ### Using PowerSync in Pages ```tsx // 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 (
    {lists.map(list =>
  • {list.name}
  • )}
); } ``` 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. ```bash npm install -D vite-plugin-wasm vite-plugin-top-level-await ``` ```ts // 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. The `exclude` configuration 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. ```tsx // WRONG — Strict Mode will destroy this on the dev double-mount function App() { const [db, setDb] = useState(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 ( ); } ``` 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 ``, not ``. Without an ErrorBoundary, query errors crash the component tree silently. ```tsx }> }> ``` ### 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.