livestorejs/livestore
View on GitHub`invoke was called before connect` error with React Router 7 Cloudflare Template
Open
#314 opened on May 19, 2025
help wanted
Repository metrics
- Stars
- (3,599 stars)
- PR merge metrics
- (PR metrics pending)
Description
Hey,
I am running into an error when I use the RR7 Cloudflare template and then try to install LiveStore.
Reproduction Steps:
- Use RR7 Cloudflare template
npx create-react-router@latest --template remix-run/react-router-templates/cloudflare
- Install Livestore packages
npm install @livestore/livestore@0.3.0-dev.50 @livestore/wa-sqlite@1.0.5-dev.2 @livestore/adapter-web@0.3.0-dev.50 @livestore/react@0.3.0-dev.50 @livestore/utils@0.3.0-dev.50 @livestore/peer-deps@0.3.0-dev.50 @livestore/devtools-vite@0.3.0-dev.50 --force
- Create Livestore worker:
app/livestore/livestore.worker.ts
import { makeWorker } from '@livestore/adapter-web/worker'
import { makeCfSync } from '@livestore/sync-cf'
import { schema } from './schema'
makeWorker({
schema,
sync: {
backend: makeCfSync({ url: import.meta.env.VITE_LIVESTORE_SYNC_URL }),
initialSyncOptions: { _tag: 'Blocking', timeout: 5000 },
},
})
- Create Livestore schema: app/livestore/schema.ts
import {
Events,
makeSchema,
Schema,
SessionIdSymbol,
State,
} from '@livestore/livestore'
// You can model your state as SQLite tables (https://dev.docs.livestore.dev/reference/state/sqlite-schema)
export const tables = {
todos: State.SQLite.table({
name: 'todos',
columns: {
id: State.SQLite.text({ primaryKey: true }),
text: State.SQLite.text({ default: '' }),
completed: State.SQLite.boolean({ default: false }),
deletedAt: State.SQLite.integer({
nullable: true,
schema: Schema.DateFromNumber,
}),
},
}),
// Client documents can be used for local-only state (e.g. form inputs)
uiState: State.SQLite.clientDocument({
name: 'uiState',
schema: Schema.Struct({
newTodoText: Schema.String,
filter: Schema.Literal('all', 'active', 'completed'),
}),
default: { id: SessionIdSymbol, value: { newTodoText: '', filter: 'all' } },
}),
}
// Events describe data changes (https://dev.docs.livestore.dev/reference/events)
export const events = {
todoCreated: Events.synced({
name: 'v1.TodoCreated',
schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
}),
todoCompleted: Events.synced({
name: 'v1.TodoCompleted',
schema: Schema.Struct({ id: Schema.String }),
}),
todoUncompleted: Events.synced({
name: 'v1.TodoUncompleted',
schema: Schema.Struct({ id: Schema.String }),
}),
todoDeleted: Events.synced({
name: 'v1.TodoDeleted',
schema: Schema.Struct({ id: Schema.String, deletedAt: Schema.Date }),
}),
todoClearedCompleted: Events.synced({
name: 'v1.TodoClearedCompleted',
schema: Schema.Struct({ deletedAt: Schema.Date }),
}),
uiStateSet: tables.uiState.set,
}
// Materializers are used to map events to state (https://dev.docs.livestore.dev/reference/state/materializers)
const materializers = State.SQLite.materializers(events, {
'v1.TodoCreated': ({ id, text }) =>
tables.todos.insert({ id, text, completed: false }),
'v1.TodoCompleted': ({ id }) =>
tables.todos.update({ completed: true }).where({ id }),
'v1.TodoUncompleted': ({ id }) =>
tables.todos.update({ completed: false }).where({ id }),
'v1.TodoDeleted': ({ id, deletedAt }) =>
tables.todos.update({ deletedAt }).where({ id }),
'v1.TodoClearedCompleted': ({ deletedAt }) =>
tables.todos.update({ deletedAt }).where({ completed: true }),
})
const state = State.SQLite.makeState({ tables, materializers })
export const schema = makeSchema({ events, state })
- Change root.tsx to:
import {
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "react-router"
import { makePersistedAdapter } from '@livestore/adapter-web'
import LiveStoreSharedWorker from '@livestore/adapter-web/shared-worker?sharedworker'
import { LiveStoreProvider } from '@livestore/react'
import type React from 'react'
import { unstable_batchedUpdates as batchUpdates } from 'react-dom'
import LiveStoreWorker from './livestore/livestore.worker?worker'
import { schema } from './livestore/schema'
import type { Route } from "./+types/root"
import "./app.css"
export const getStoreId = () => {
if (typeof window === 'undefined') return 'unused'
const searchParams = new URLSearchParams(window.location.search)
const storeId = searchParams.get('storeId')
if (storeId !== null) return storeId
const newAppId = crypto.randomUUID()
searchParams.set('storeId', newAppId)
window.location.search = searchParams.toString()
}
export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
rel: "preconnect",
href: "https://fonts.gstatic.com",
crossOrigin: "anonymous",
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
]
const storeId = getStoreId()
const adapter = makePersistedAdapter({
storage: { type: 'opfs' },
worker: LiveStoreWorker,
sharedWorker: LiveStoreSharedWorker,
})
export function Layout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<LiveStoreProvider
schema={schema}
adapter={adapter}
renderLoading={(_) => <div>Loading LiveStore ({_.stage})...</div>}
batchUpdates={batchUpdates}
storeId={storeId}
syncPayload={{ authToken: 'insecure-token-change-me' }}
>
{children}
<ScrollRestoration />
<Scripts />
</LiveStoreProvider>
</body>
</html >
)
}
export default function App() {
return <Outlet />
}
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
let message = "Oops!"
let details = "An unexpected error occurred."
let stack: string | undefined
if (isRouteErrorResponse(error)) {
message = error.status === 404 ? "404" : "Error"
details =
error.status === 404
? "The requested page could not be found."
: error.statusText || details
} else if (import.meta.env.DEV && error && error instanceof Error) {
details = error.message
stack = error.stack
}
return (
<main className="pt-16 p-4 container mx-auto">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<code>{stack}</code>
</pre>
)}
</main>
)
}
- Change vite.config.ts to
import { reactRouter } from '@react-router/dev/vite'
import { cloudflare } from '@cloudflare/vite-plugin'
import tailwindcss from '@tailwindcss/vite'
import { defineConfig } from 'vite'
import { spawn } from 'node:child_process'
import { livestoreDevtoolsPlugin } from '@livestore/devtools-vite'
export default defineConfig({
server: {
port: process.env.PORT ? Number(process.env.PORT) : 60_001,
},
worker: { format: 'es' },
plugins: [
livestoreDevtoolsPlugin({ schemaPath: './app/livestore/schema.ts' }),
cloudflare({ viteEnvironment: { name: 'ssr' } }),
tailwindcss(),
reactRouter(),
// Running `wrangler dev` as part of `vite dev` needed for `@livestore/sync-cf`
{
name: 'wrangler-dev',
configureServer: async (server) => {
const wrangler = spawn(
'./node_modules/.bin/wrangler',
['dev', '--port', '8787'],
{
stdio: ['ignore', 'inherit', 'inherit'],
shell: true,
}
)
server.httpServer?.on('close', () => wrangler.kill())
wrangler.on('exit', (code) =>
console.error(`wrangler dev exited with code ${code}`)
)
},
},
],
})
npm run dev
See error: invoke was called before connect
Repro link: https://github.com/LydiaF/livestore-rr7-cf-example