@livestore/livestore 0.4.0-dev.21 → 0.4.0-dev.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/effect/LiveStore.d.ts +123 -2
  3. package/dist/effect/LiveStore.d.ts.map +1 -1
  4. package/dist/effect/LiveStore.js +195 -1
  5. package/dist/effect/LiveStore.js.map +1 -1
  6. package/dist/effect/mod.d.ts +1 -1
  7. package/dist/effect/mod.d.ts.map +1 -1
  8. package/dist/effect/mod.js +3 -1
  9. package/dist/effect/mod.js.map +1 -1
  10. package/dist/mod.d.ts +1 -0
  11. package/dist/mod.d.ts.map +1 -1
  12. package/dist/mod.js +1 -0
  13. package/dist/mod.js.map +1 -1
  14. package/dist/store/StoreRegistry.d.ts +190 -0
  15. package/dist/store/StoreRegistry.d.ts.map +1 -0
  16. package/dist/store/StoreRegistry.js +244 -0
  17. package/dist/store/StoreRegistry.js.map +1 -0
  18. package/dist/store/StoreRegistry.test.d.ts +2 -0
  19. package/dist/store/StoreRegistry.test.d.ts.map +1 -0
  20. package/dist/store/StoreRegistry.test.js +380 -0
  21. package/dist/store/StoreRegistry.test.js.map +1 -0
  22. package/dist/store/create-store.d.ts +50 -4
  23. package/dist/store/create-store.d.ts.map +1 -1
  24. package/dist/store/create-store.js +19 -0
  25. package/dist/store/create-store.js.map +1 -1
  26. package/dist/store/devtools.d.ts.map +1 -1
  27. package/dist/store/devtools.js +13 -0
  28. package/dist/store/devtools.js.map +1 -1
  29. package/dist/store/store-types.d.ts +10 -25
  30. package/dist/store/store-types.d.ts.map +1 -1
  31. package/dist/store/store-types.js.map +1 -1
  32. package/dist/store/store.d.ts +23 -6
  33. package/dist/store/store.d.ts.map +1 -1
  34. package/dist/store/store.js +20 -2
  35. package/dist/store/store.js.map +1 -1
  36. package/docs/building-with-livestore/complex-ui-state/index.md +0 -2
  37. package/docs/building-with-livestore/crud/index.md +0 -2
  38. package/docs/building-with-livestore/data-modeling/index.md +29 -0
  39. package/docs/building-with-livestore/examples/todo-workspaces/index.md +0 -6
  40. package/docs/building-with-livestore/opentelemetry/index.md +25 -6
  41. package/docs/building-with-livestore/rules-for-ai-agents/index.md +2 -2
  42. package/docs/building-with-livestore/state/sql-queries/index.md +22 -0
  43. package/docs/building-with-livestore/state/sqlite-schema/index.md +2 -2
  44. package/docs/building-with-livestore/store/index.md +344 -0
  45. package/docs/framework-integrations/react-integration/index.md +380 -361
  46. package/docs/framework-integrations/vue-integration/index.md +2 -2
  47. package/docs/getting-started/expo/index.md +189 -43
  48. package/docs/getting-started/react-web/index.md +77 -24
  49. package/docs/getting-started/vue/index.md +3 -3
  50. package/docs/index.md +1 -2
  51. package/docs/llms.txt +0 -1
  52. package/docs/misc/troubleshooting/index.md +3 -3
  53. package/docs/overview/how-livestore-works/index.md +1 -1
  54. package/docs/overview/introduction/index.md +409 -1
  55. package/docs/overview/why-livestore/index.md +108 -2
  56. package/docs/patterns/auth/index.md +185 -34
  57. package/docs/patterns/effect/index.md +11 -1
  58. package/docs/patterns/storybook/index.md +43 -26
  59. package/docs/platform-adapters/expo-adapter/index.md +36 -19
  60. package/docs/platform-adapters/web-adapter/index.md +71 -2
  61. package/docs/tutorial/1-setup-starter-project/index.md +5 -5
  62. package/docs/tutorial/3-read-and-write-todos-via-livestore/index.md +54 -35
  63. package/docs/tutorial/5-expand-business-logic/index.md +1 -1
  64. package/docs/tutorial/6-persist-ui-state/index.md +12 -12
  65. package/package.json +6 -6
  66. package/src/effect/LiveStore.ts +385 -3
  67. package/src/effect/mod.ts +13 -1
  68. package/src/mod.ts +1 -0
  69. package/src/store/StoreRegistry.test.ts +516 -0
  70. package/src/store/StoreRegistry.ts +393 -0
  71. package/src/store/create-store.ts +50 -4
  72. package/src/store/devtools.ts +15 -0
  73. package/src/store/store-types.ts +17 -5
  74. package/src/store/store.ts +25 -5
  75. package/docs/building-with-livestore/examples/index.md +0 -30
@@ -7,26 +7,111 @@ While LiveStore is framework agnostic, the `@livestore/react` package provides a
7
7
  - High performance
8
8
  - Fine-grained reactivity (using LiveStore's signals-based reactivity system)
9
9
  - Instant, synchronous query results (without the need for `useEffect` and `isLoading` checks)
10
+ - Supports multiple store instances
10
11
  - Transactional state transitions (via `batchUpdates`)
11
12
  - Also supports Expo / React Native via `@livestore/adapter-expo`
12
13
 
13
- ## API
14
+ ## Core Concepts
14
15
 
15
- ### `LiveStoreProvider`
16
+ When using LiveStore in React, you'll primarily interact with these fundamental components:
16
17
 
17
- In order to use LiveStore with React, you need to wrap your application in a `LiveStoreProvider`.
18
+ - [**`StoreRegistry`**](#new-storeregistryconfig) - Manages store instances with automatic caching and disposal
19
+ - [**`<StoreRegistryProvider>`**](#storeregistryprovider) - React context provider that supplies the registry to components
20
+ - [**`useStore()`**](#usestoreoptions) - Suspense-enabled hook for accessing store instances
18
21
 
19
- ## `reference/framework-integrations/react/provider.tsx`
22
+ Stores are cached by their `storeId` and automatically disposed after being unused for a configurable duration (`unusedCacheTime`).
20
23
 
21
- ```tsx filename="reference/framework-integrations/react/provider.tsx"
24
+ ## `reference/framework-integrations/react/multi-store/minimal.tsx`
25
+
26
+ ```tsx filename="reference/framework-integrations/react/multi-store/minimal.tsx"
27
+
28
+ const issueStoreOptions = (issueId: string) =>
29
+ storeOptions({
30
+ storeId: `issue-${issueId}`,
31
+ schema,
32
+ adapter: makeInMemoryAdapter(),
33
+ })
34
+
35
+ export function App() {
36
+ const [storeRegistry] = useState(() => new StoreRegistry({ defaultOptions: { batchUpdates } }))
37
+ return (
38
+ <StoreRegistryProvider storeRegistry={storeRegistry}>
39
+ <IssueView />
40
+ </StoreRegistryProvider>
41
+ )
42
+ }
43
+
44
+ function IssueView() {
45
+ const store = useStore(issueStoreOptions('abc123'))
46
+ const [issue] = store.useQuery(queryDb(tables.issue.select()))
47
+ return <div>{issue?.title}</div>
48
+ }
49
+ ```
50
+
51
+ ### `reference/framework-integrations/react/multi-store/schema.ts`
52
+
53
+ ```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
54
+
55
+ // Event definitions
56
+ export const events = {
57
+ issueCreated: Events.synced({
58
+ name: 'v1.IssueCreated',
59
+ schema: Schema.Struct({
60
+ id: Schema.String,
61
+ title: Schema.String,
62
+ status: Schema.Literal('todo', 'done'),
63
+ }),
64
+ }),
65
+ issueStatusChanged: Events.synced({
66
+ name: 'v1.IssueStatusChanged',
67
+ schema: Schema.Struct({
68
+ id: Schema.String,
69
+ status: Schema.Literal('todo', 'done'),
70
+ }),
71
+ }),
72
+ }
73
+
74
+ // State definition
75
+ export const tables = {
76
+ issue: State.SQLite.table({
77
+ name: 'issue',
78
+ columns: {
79
+ id: State.SQLite.text({ primaryKey: true }),
80
+ title: State.SQLite.text(),
81
+ status: State.SQLite.text(),
82
+ },
83
+ }),
84
+ }
85
+
86
+ const materializers = State.SQLite.materializers(events, {
87
+ 'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
88
+ 'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
89
+ })
90
+
91
+ const state = State.SQLite.makeState({ tables, materializers })
92
+
93
+ export const schema = makeSchema({ events, state })
94
+ ```
95
+
96
+ ## Setting Up
97
+
98
+ ### 1. Configure the Store
99
+
100
+ Create a store configuration file that exports a custom hook wrapping [`useStore()`](#usestoreoptions):
101
+
102
+ ## `reference/framework-integrations/react/store.ts`
103
+
104
+ ```ts filename="reference/framework-integrations/react/store.ts"
22
105
 
23
106
  const adapter = makeInMemoryAdapter()
24
107
 
25
- export const Root: FC<{ children: ReactNode }> = ({ children }) => (
26
- <LiveStoreProvider schema={schema} adapter={adapter} batchUpdates={batchUpdates}>
27
- {children}
28
- </LiveStoreProvider>
29
- )
108
+ export const useAppStore = () =>
109
+ useStore({
110
+ storeId: 'app-root',
111
+ schema,
112
+ adapter,
113
+ batchUpdates,
114
+ })
30
115
  ```
31
116
 
32
117
  ### `reference/framework-integrations/react/schema.ts`
@@ -67,46 +152,39 @@ const state = State.SQLite.makeState({ tables, materializers })
67
152
  export const schema = makeSchema({ events, state })
68
153
  ```
69
154
 
70
- #### Logging
71
-
72
- `LiveStoreProvider` accepts optional logging configuration:
73
-
74
- ```tsx
155
+ The [`useStore()`](#usestoreoptions) hook accepts store configuration options and returns a store instance. It suspends while the store is loading, so components using it need to be wrapped in a `Suspense` boundary.
75
156
 
76
- <LiveStoreProvider
77
- schema={schema}
78
- adapter={adapter}
79
- batchUpdates={batchUpdates}
80
- // Optional: swap the logger implementation
81
- logger={Logger.prettyWithThread('app')}
82
- // Optional: set minimum log level (use LogLevel.None to disable)
83
- logLevel={LogLevel.Info}
84
- >
85
- <App />
86
- </LiveStoreProvider>
87
- ```
157
+ ### 2. Set Up the Registry
88
158
 
89
- For scenarios where you have an existing store instance, you can manually create a `LiveStoreContext.Provider`:
159
+ Create a [`StoreRegistry`](#new-storeregistryconfig) and provide it via [`<StoreRegistryProvider>`](#storeregistryprovider). Wrap in a `<Suspense>` to handle loading states and a `<ErrorBoundary>` to handle errors:
90
160
 
91
- ## `reference/framework-integrations/react/context-provider.tsx`
161
+ ## `reference/framework-integrations/react/provider.tsx`
92
162
 
93
- ```tsx filename="reference/framework-integrations/react/context-provider.tsx"
163
+ ```tsx filename="reference/framework-integrations/react/provider.tsx"
94
164
 
95
- declare const store: Store & ReactApi
165
+ export const Root: FC<{ children: ReactNode }> = ({ children }) => {
166
+ const [storeRegistry] = useState(() => new StoreRegistry())
96
167
 
97
- export const Root: FC<{ children: ReactNode }> = ({ children }) => (
98
- <LiveStoreContext.Provider value={{ stage: 'running', store }}>{children}</LiveStoreContext.Provider>
99
- )
168
+ return (
169
+ <ErrorBoundary fallback={<div>Something went wrong</div>}>
170
+ <Suspense fallback={<div>Loading LiveStore...</div>}>
171
+ <StoreRegistryProvider storeRegistry={storeRegistry}>{children}</StoreRegistryProvider>
172
+ </Suspense>
173
+ </ErrorBoundary>
174
+ )
175
+ }
100
176
  ```
101
177
 
102
- ### useStore
178
+ ### 3. Use the Store
179
+
180
+ Components can now access the store via your custom hook:
103
181
 
104
182
  ## `reference/framework-integrations/react/use-store.tsx`
105
183
 
106
184
  ```tsx filename="reference/framework-integrations/react/use-store.tsx"
107
185
 
108
186
  export const MyComponent: FC = () => {
109
- const { store } = useStore()
187
+ const store = useAppStore()
110
188
 
111
189
  useEffect(() => {
112
190
  store.commit(events.todoCreated({ id: '1', text: 'Hello, world!' }))
@@ -154,7 +232,24 @@ const state = State.SQLite.makeState({ tables, materializers })
154
232
  export const schema = makeSchema({ events, state })
155
233
  ```
156
234
 
157
- ### useQuery
235
+ ### `reference/framework-integrations/react/store.ts`
236
+
237
+ ```ts filename="reference/framework-integrations/react/store.ts"
238
+
239
+ const adapter = makeInMemoryAdapter()
240
+
241
+ export const useAppStore = () =>
242
+ useStore({
243
+ storeId: 'app-root',
244
+ schema,
245
+ adapter,
246
+ batchUpdates,
247
+ })
248
+ ```
249
+
250
+ ## Querying Data
251
+
252
+ Use [`store.useQuery()`](#storeusequeryqueryable) to subscribe to reactive queries:
158
253
 
159
254
  ## `reference/framework-integrations/react/use-query.tsx`
160
255
 
@@ -165,7 +260,7 @@ const query$ = queryDb(tables.todos.where({ completed: true }).orderBy('id', 'de
165
260
  })
166
261
 
167
262
  export const CompletedTodos: FC = () => {
168
- const { store } = useStore()
263
+ const store = useAppStore()
169
264
  const todos = store.useQuery(query$)
170
265
 
171
266
  return (
@@ -216,14 +311,31 @@ const state = State.SQLite.makeState({ tables, materializers })
216
311
  export const schema = makeSchema({ events, state })
217
312
  ```
218
313
 
219
- ### useClientDocument
314
+ ### `reference/framework-integrations/react/store.ts`
315
+
316
+ ```ts filename="reference/framework-integrations/react/store.ts"
317
+
318
+ const adapter = makeInMemoryAdapter()
319
+
320
+ export const useAppStore = () =>
321
+ useStore({
322
+ storeId: 'app-root',
323
+ schema,
324
+ adapter,
325
+ batchUpdates,
326
+ })
327
+ ```
328
+
329
+ ## Client Documents
330
+
331
+ Use [`store.useClientDocument()`](#storeuseclientdocumenttable-id-options) for client-specific state:
220
332
 
221
333
  ## `reference/framework-integrations/react/use-client-document.tsx`
222
334
 
223
335
  ```tsx filename="reference/framework-integrations/react/use-client-document.tsx"
224
336
 
225
337
  export const TodoItem: FC<{ id: string }> = ({ id }) => {
226
- const { store } = useStore()
338
+ const store = useAppStore()
227
339
  const [todo, updateTodo] = store.useClientDocument(tables.uiState, id)
228
340
 
229
341
  return (
@@ -272,182 +384,31 @@ const state = State.SQLite.makeState({ tables, materializers })
272
384
  export const schema = makeSchema({ events, state })
273
385
  ```
274
386
 
275
- ## Usage with ...
387
+ ### `reference/framework-integrations/react/store.ts`
276
388
 
277
- ### Vite
278
-
279
- LiveStore works with Vite out of the box.
280
-
281
- ### Tanstack Start
282
-
283
- LiveStore works with Tanstack Start out of the box.
284
-
285
- #### Notes
286
-
287
- When using LiveStore with TanStack Start, it's crucial to place `LiveStoreProvider` in the correct location to avoid remounting on navigation.
288
-
289
- :::warn
290
- **Do NOT place `LiveStoreProvider` inside `shellComponent`**. The `shellComponent` can be re-rendered on navigation, causing LiveStore to remount and show the loading screen on every page transition.
291
- :::
292
-
293
- #### Correct pattern
294
-
295
- Use the `component` prop on `createRootRoute` for `LiveStoreProvider`:
296
-
297
- ```tsx
298
-
299
- export const Route = createRootRoute({
300
- shellComponent: RootShell, // HTML structure only - NO state or providers
301
- component: RootComponent, // App shell - LiveStoreProvider goes HERE
302
- })
303
-
304
- // HTML document shell - keep this stateless
305
- function RootShell({ children }: { children: React.ReactNode }) {
306
- return (
307
- <html lang="en">
308
- <head><HeadContent /></head>
309
- <body>
310
- {children}
311
- <Scripts />
312
- </body>
313
- </html>
314
- )
315
- }
316
-
317
- // App shell - persists across SPA navigation
318
- function RootComponent() {
319
- return (
320
- <LiveStoreProvider
321
- schema={schema}
322
- adapter={adapter}
323
- batchUpdates={batchUpdates}
324
- >
325
- <Outlet />
326
- </LiveStoreProvider>
327
- )
328
- }
329
- ```
330
-
331
- #### Why this matters
332
-
333
- TanStack Start's `shellComponent` is designed for SSR HTML streaming and may be re-evaluated on server requests during navigation. When `LiveStoreProvider` is placed there:
334
-
335
- - The WebSocket connection is re-established on each navigation
336
- - The "Loading LiveStore" screen appears
337
- - All LiveStore state is re-initialized
338
-
339
- The `component` prop creates a React component that stays mounted during client-side SPA navigation, preserving LiveStore's connection and state.
340
-
341
- #### Debugging
342
-
343
- If you see the loading screen on every navigation, check your server logs. Multiple "Launching WebSocket" messages indicate `LiveStoreProvider` is remounting incorrectly.
344
-
345
- ### Expo / React Native
346
-
347
- LiveStore has a first-class integration with Expo / React Native via `@livestore/adapter-expo`.
348
-
349
- ### Next.js
350
-
351
- Given various Next.js limitations, LiveStore doesn't yet work with Next.js out of the box.
352
-
353
- ## Multi-Store
354
-
355
- The multi-store API enables managing multiple stores within a single React application. This is useful for:
389
+ ```ts filename="reference/framework-integrations/react/store.ts"
356
390
 
357
- - **Partial data synchronization** - Load only the data you need, when you need it
358
- - **Multi-tenant applications** - Separate stores for each workspace, organization, or project (like Slack workspaces, Notion pages, or Linear teams)
359
-
360
- ## `reference/framework-integrations/react/multi-store/minimal.tsx`
361
-
362
- ```tsx filename="reference/framework-integrations/react/multi-store/minimal.tsx"
391
+ const adapter = makeInMemoryAdapter()
363
392
 
364
- const issueStoreOptions = (issueId: string) =>
365
- storeOptions({
366
- storeId: `issue-${issueId}`,
393
+ export const useAppStore = () =>
394
+ useStore({
395
+ storeId: 'app-root',
367
396
  schema,
368
- adapter: makeInMemoryAdapter(),
397
+ adapter,
398
+ batchUpdates,
369
399
  })
370
-
371
- export function App() {
372
- const [registry] = useState(() => new StoreRegistry({ defaultOptions: { batchUpdates } }))
373
- return (
374
- <StoreRegistryProvider storeRegistry={registry}>
375
- <IssueView />
376
- </StoreRegistryProvider>
377
- )
378
- }
379
-
380
- function IssueView() {
381
- const store = useStore(issueStoreOptions('abc123'))
382
- const [issue] = store.useQuery(queryDb(tables.issue.select()))
383
- return <div>{issue?.title}</div>
384
- }
385
- ```
386
-
387
- ### `reference/framework-integrations/react/multi-store/schema.ts`
388
-
389
- ```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
390
-
391
- // Event definitions
392
- export const events = {
393
- issueCreated: Events.synced({
394
- name: 'v1.IssueCreated',
395
- schema: Schema.Struct({
396
- id: Schema.String,
397
- title: Schema.String,
398
- status: Schema.Literal('todo', 'done'),
399
- }),
400
- }),
401
- issueStatusChanged: Events.synced({
402
- name: 'v1.IssueStatusChanged',
403
- schema: Schema.Struct({
404
- id: Schema.String,
405
- status: Schema.Literal('todo', 'done'),
406
- }),
407
- }),
408
- }
409
-
410
- // State definition
411
- export const tables = {
412
- issue: State.SQLite.table({
413
- name: 'issue',
414
- columns: {
415
- id: State.SQLite.text({ primaryKey: true }),
416
- title: State.SQLite.text(),
417
- status: State.SQLite.text(),
418
- },
419
- }),
420
- }
421
-
422
- const materializers = State.SQLite.materializers(events, {
423
- 'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
424
- 'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
425
- })
426
-
427
- const state = State.SQLite.makeState({ tables, materializers })
428
-
429
- export const schema = makeSchema({ events, state })
430
400
  ```
431
401
 
432
- :::caution[Experimental API]
433
- The Multi-Store API is still early in its development.
402
+ ## Advanced Patterns
434
403
 
435
- If you have feedback or questions about this API, please don't hesitate to comment on the [RFC](https://github.com/livestorejs/livestore/pull/585)
436
- :::
437
-
438
- ### Core Concepts
439
-
440
- The multi-store API introduces four main primitives:
441
-
442
- - **StoreRegistry** - Manages and caches all store instances with automatic garbage collection
443
- - **useStore()** - Suspense-enabled hook for accessing individual store instances
444
- - **storeOptions()** - Type-safe way to define reusable store configurations
404
+ ### Multiple Stores
445
405
 
446
- Stores are cached by their `storeId` and automatically disposed after being unused for a configurable duration (`unusedCacheTime`)
406
+ You can have multiple stores within a single React application. This is useful for:
447
407
 
448
- ### Setting Up
408
+ - **Partial data synchronization** - Load only the data you need, when you need it
409
+ - **Multi-tenant applications** - Separate stores for each workspace, organization, or team (like Slack workspaces or Linear teams)
449
410
 
450
- First, define your re-usable store configuration using `storeOptions()`:
411
+ Use the [`storeOptions()`](#storeoptionsoptions) helper for type-safe, reusable configurations, and then create multiple instances for the same store configuration by using different `storeId` values:
451
412
 
452
413
  ## `reference/framework-integrations/react/multi-store/store.ts`
453
414
 
@@ -508,23 +469,6 @@ const state = State.SQLite.makeState({ tables, materializers })
508
469
  export const schema = makeSchema({ events, state })
509
470
  ```
510
471
 
511
- Then create a `StoreRegistry` and provide it to your app:
512
-
513
- ## `reference/framework-integrations/react/multi-store/App.tsx`
514
-
515
- ```tsx filename="reference/framework-integrations/react/multi-store/App.tsx"
516
-
517
- export function App({ children }: { children: ReactNode }) {
518
- const [storeRegistry] = useState(() => new StoreRegistry({ defaultOptions: { batchUpdates } }))
519
-
520
- return <StoreRegistryProvider storeRegistry={storeRegistry}>{children}</StoreRegistryProvider>
521
- }
522
- ```
523
-
524
- ### Using Stores
525
-
526
- Use the `useStore()` hook to load or get a store instance. It suspends until the store is loaded:
527
-
528
472
  ## `reference/framework-integrations/react/multi-store/IssueView.tsx`
529
473
 
530
474
  ```tsx filename="reference/framework-integrations/react/multi-store/IssueView.tsx"
@@ -618,113 +562,11 @@ export const issueStoreOptions = (issueId: string) =>
618
562
  })
619
563
  ```
620
564
 
621
- ### Multiple Instances
622
-
623
- You can create multiple instances of the same store type by using different `storeId` values:
624
-
625
- ## `reference/framework-integrations/react/multi-store/IssueList.tsx`
626
-
627
- ```tsx filename="reference/framework-integrations/react/multi-store/IssueList.tsx"
628
-
629
- function IssueCard({ issueId }: { issueId: string }) {
630
- // Each call to useStore with a different storeId loads/gets a separate store instance
631
- const issueStore = useStore(issueStoreOptions(issueId))
632
- const [issue] = issueStore.useQuery(queryDb(tables.issue.select().where({ id: issueId })))
633
-
634
- if (!issue) return <div>Issue not found</div>
635
-
636
- return (
637
- <div>
638
- <h4>{issue.title}</h4>
639
- <p>Store ID: {issueStore.storeId}</p>
640
- <p>Status: {issue.status}</p>
641
- </div>
642
- )
643
- }
644
-
645
- // Component that displays multiple independent store instances with shared error and loading states
646
- export function IssueList() {
647
- const issueIds = ['issue-1', 'issue-2', 'issue-3']
648
-
649
- return (
650
- <div>
651
- <h3>Issues</h3>
652
- <ErrorBoundary fallback={<div>Error loading issues</div>}>
653
- <Suspense fallback={<div>Loading issues...</div>}>
654
- {issueIds.map((id) => (
655
- <IssueCard key={id} issueId={id} />
656
- ))}
657
- </Suspense>
658
- </ErrorBoundary>
659
- </div>
660
- )
661
- }
662
- ```
663
-
664
- ### `reference/framework-integrations/react/multi-store/schema.ts`
665
-
666
- ```ts filename="reference/framework-integrations/react/multi-store/schema.ts"
667
-
668
- // Event definitions
669
- export const events = {
670
- issueCreated: Events.synced({
671
- name: 'v1.IssueCreated',
672
- schema: Schema.Struct({
673
- id: Schema.String,
674
- title: Schema.String,
675
- status: Schema.Literal('todo', 'done'),
676
- }),
677
- }),
678
- issueStatusChanged: Events.synced({
679
- name: 'v1.IssueStatusChanged',
680
- schema: Schema.Struct({
681
- id: Schema.String,
682
- status: Schema.Literal('todo', 'done'),
683
- }),
684
- }),
685
- }
686
-
687
- // State definition
688
- export const tables = {
689
- issue: State.SQLite.table({
690
- name: 'issue',
691
- columns: {
692
- id: State.SQLite.text({ primaryKey: true }),
693
- title: State.SQLite.text(),
694
- status: State.SQLite.text(),
695
- },
696
- }),
697
- }
698
-
699
- const materializers = State.SQLite.materializers(events, {
700
- 'v1.IssueCreated': ({ id, title, status }) => tables.issue.insert({ id, title, status }),
701
- 'v1.IssueStatusChanged': ({ id, status }) => tables.issue.update({ status }).where({ id }),
702
- })
703
-
704
- const state = State.SQLite.makeState({ tables, materializers })
705
-
706
- export const schema = makeSchema({ events, state })
707
- ```
708
-
709
- ### `reference/framework-integrations/react/multi-store/store.ts`
710
-
711
- ```ts filename="reference/framework-integrations/react/multi-store/store.ts"
712
-
713
- // Define reusable store configuration with storeOptions()
714
- // This helper provides type safety and can be reused across your app
715
- export const issueStoreOptions = (issueId: string) =>
716
- storeOptions({
717
- storeId: `issue-${issueId}`,
718
- schema,
719
- adapter: makeInMemoryAdapter(),
720
- })
721
- ```
722
-
723
565
  Each store instance is completely isolated with its own data, event log, and synchronization state.
724
566
 
725
- ### Preloading
567
+ ### Preloading Stores
726
568
 
727
- When you know a store will be needed soon, you can preload it in advance:
569
+ When you know a store will be needed soon, preload it in advance to warm up the cache:
728
570
 
729
571
  ## `reference/framework-integrations/react/multi-store/PreloadedIssue.tsx`
730
572
 
@@ -734,9 +576,12 @@ export function PreloadedIssue({ issueId }: { issueId: string }) {
734
576
  const [showIssue, setShowIssue] = useState(false)
735
577
  const storeRegistry = useStoreRegistry()
736
578
 
737
- // Preload the store when user hovers (before they click)
579
+ // Preload the store when the user hovers (before they click)
738
580
  const handleMouseEnter = () => {
739
- storeRegistry.preload(issueStoreOptions(issueId))
581
+ storeRegistry.preload({
582
+ ...issueStoreOptions(issueId),
583
+ unusedCacheTime: 10_000, // Optionally override options
584
+ })
740
585
  }
741
586
 
742
587
  return (
@@ -850,69 +695,243 @@ export const issueStoreOptions = (issueId: string) =>
850
695
  })
851
696
  ```
852
697
 
853
- This warms up the cache so the store is ready when the user navigates to it.
854
-
855
698
  ### StoreId Guidelines
856
699
 
857
700
  When creating `storeId` values:
858
701
 
859
- - **Use namespaces** - Prefix with the entity type (e.g., `workspace-abc123`, `issue-456`) to avoid collisions between different store types and improve debugging
702
+ - **Valid characters** - Only alphanumeric characters, underscores (`_`), and hyphens (`-`) are allowed (regex: `/^[a-zA-Z0-9_-]+$/`)
860
703
  - **Globally unique** - Prefer globally unique IDs (e.g., nanoid) to prevent collisions
704
+ - **Use namespaces** - Prefix with the entity type (e.g., `workspace-abc123`, `issue-456`) to avoid collisions and easier identification when debugging
861
705
  - **Keep them stable** - The same entity should always use the same `storeId` across renders
862
- - **Sanitize user input** - If incorporating user data, be sure to validate/sanitize to prevent injection attacks
863
- - **Document your conventions** - Document your conventions and special IDs like `user-current` as they're part of your API contract
706
+ - **Sanitize user input** - If incorporating user data, validate/sanitize to prevent injection attacks
707
+ - **Document your conventions** - Document special IDs like `user-current` as they're part of your API contract
708
+
709
+ ### Logging
710
+
711
+ You can customize the logger and log level for debugging:
712
+
713
+ ## `reference/framework-integrations/react/store-with-logging.ts`
714
+
715
+ ```ts filename="reference/framework-integrations/react/store-with-logging.ts"
716
+
717
+ const adapter = makeInMemoryAdapter()
718
+
719
+ // ---cut---
720
+ export const useAppStore = () =>
721
+ useStore({
722
+ storeId: 'app-root',
723
+ schema,
724
+ adapter,
725
+ batchUpdates,
726
+ // Optional: swap the logger implementation
727
+ logger: Logger.prettyWithThread('app'),
728
+ // Optional: set minimum log level (use LogLevel.None to disable)
729
+ logLevel: LogLevel.Info,
730
+ })
731
+ ```
732
+
733
+ ### `reference/framework-integrations/react/schema.ts`
734
+
735
+ ```ts filename="reference/framework-integrations/react/schema.ts"
736
+
737
+ export const tables = {
738
+ todos: State.SQLite.table({
739
+ name: 'todos',
740
+ columns: {
741
+ id: State.SQLite.text({ primaryKey: true }),
742
+ text: State.SQLite.text(),
743
+ completed: State.SQLite.boolean({ default: false }),
744
+ },
745
+ }),
746
+ uiState: State.SQLite.clientDocument({
747
+ name: 'UiState',
748
+ schema: Schema.Struct({ text: Schema.String }),
749
+ default: { value: { text: '' } },
750
+ }),
751
+ } as const
752
+
753
+ export const events = {
754
+ todoCreated: Events.synced({
755
+ name: 'v1.TodoCreated',
756
+ schema: Schema.Struct({ id: Schema.String, text: Schema.String }),
757
+ }),
758
+ } as const
759
+
760
+ const materializers = State.SQLite.materializers(events, {
761
+ [events.todoCreated.name]: defineMaterializer(events.todoCreated, ({ id, text }) =>
762
+ tables.todos.insert({ id, text, completed: false }),
763
+ ),
764
+ })
765
+
766
+ const state = State.SQLite.makeState({ tables, materializers })
767
+
768
+ export const schema = makeSchema({ events, state })
769
+ ```
770
+
771
+ Use `LogLevel.None` to disable logging entirely.
772
+
773
+ ## API Reference
864
774
 
865
- ### API Reference
775
+ ### `storeOptions(options)`
866
776
 
867
- #### `storeOptions(options)`
777
+ Helper for defining reusable store options with full type inference. Returns options that can be passed to [`useStore()`](#usestoreoptions) or [`storeRegistry.preload()`](#storeregistrypreloadoptions).
868
778
 
869
- Defines reusable store configuration with type safety. Returns options that can be passed to `useStore()` or `registry.preload()`.
779
+ Options:
780
+ - `storeId` - Unique identifier for this store instance
781
+ - `schema` - The LiveStore schema
782
+ - `adapter` - The platform adapter
783
+ - `unusedCacheTime?` - Time in ms to keep unused stores in cache (default: `60_000` in browser, `Infinity` in non-browser environments)
784
+ - `batchUpdates?` - Function for batching React updates (recommended)
785
+ - `boot?` - Function called when the store is loaded
786
+ - `onBootStatus?` - Callback for boot status updates
787
+ - `context?` - User-defined context for dependency injection
788
+ - `syncPayload?` - Payload sent to sync backend (e.g., auth tokens)
789
+ - `syncPayloadSchema?` - Schema for type-safe sync payload validation
790
+ - `confirmUnsavedChanges?` - Register beforeunload handler (default: `true`, web only)
791
+ - `logger?` - Custom logger implementation
792
+ - `logLevel?` - Log level (e.g., `LogLevel.Info`, `LogLevel.Debug`, `LogLevel.None`)
793
+ - `otelOptions?` - OpenTelemetry configuration (`{ tracer, rootSpanContext }`)
794
+ - `disableDevtools?` - Whether to disable devtools (`boolean | 'auto'`, default: `'auto'`)
795
+ - `debug?` - Debug options (`{ instanceId?: string }`)
796
+
797
+ ### `useStore(options)`
798
+
799
+ Returns a store instance augmented with hooks ([`store.useQuery()`](#storeusequeryqueryable) and [`store.useClientDocument()`](#storeuseclientdocumenttable-id-options)) for reactive queries.
800
+
801
+ - Suspends until the store is loaded.
802
+ - Throws an error if loading fails.
803
+ - Store gets cached by its `storeId` in the [`StoreRegistry`](#new-storeregistryconfig). Multiple calls with the same `storeId` return the same store instance.
804
+ - Store is cached as long as it's being used, and after `unusedCacheTime` expires (default `60_000` ms in browser, `Infinity` in non-browser)
805
+ - Default store options can be configured in [`StoreRegistry`](#new-storeregistryconfig) constructor.
806
+ - Store options are only applied when the store is loaded. Subsequent calls with different options will not affect the store if it's already loaded and cached in the registry.
807
+
808
+ ### `store.commit(...events)` / `store.commit(txnFn)`
809
+
810
+ Commits events to the store. Supports multiple calling patterns:
811
+
812
+ - `store.commit(...events)` - Commit one or more events
813
+ - `store.commit(txnFn)` - Commit events via a transaction function
814
+ - `store.commit(options, ...events)` - Commit with options
815
+ - `store.commit(options, txnFn)` - Options with transaction function
870
816
 
871
817
  Options:
872
- - `storeId` - Unique identifier for this store instance (required)
873
- - `schema` - The LiveStore schema (required)
874
- - `adapter` - The platform adapter (required)
875
- - `unusedCacheTime` - Time in milliseconds to keep unused stores in memory (default: 60_000 in browser, infinity in non-browser)
876
- - `boot` - Function called when the store is first loaded
877
- - `batchUpdates` - Function for batching React updates
878
- - And other `CreateStoreOptions`
818
+ - `skipRefresh` - Skip refreshing reactive queries after commit (advanced)
819
+
820
+ ### `store.useQuery(queryable)`
821
+
822
+ Subscribes to a reactive query. Re-renders the component when the result changes.
823
+
824
+ - Takes any `Queryable`: `QueryBuilder`, `LiveQueryDef`, `SignalDef`, or `LiveQuery` instance
825
+ - Returns the query result
826
+
827
+ ### `store.useClientDocument(table, id?, options?)`
828
+
829
+ React.useState-like hook for client-document tables.
830
+
831
+ - Returns `[row, setRow, id, query$]` tuple
832
+ - Works only with tables defined via `State.SQLite.clientDocument()`
833
+ - If the table has a default id, the `id` argument is optional
879
834
 
880
- #### `new StoreRegistry(config?)`
835
+ ### `new StoreRegistry(config?)`
881
836
 
882
- Creates a registry that manages store instances.
837
+ Creates a registry that coordinates store loading, caching, and retention.
883
838
 
884
839
  Config:
885
- - `defaultOptions` - Default options applied to all stores (can be overridden per-store)
840
+ - `defaultOptions?` - Default options that are applied to all stores when they are loaded.:
841
+ - `batchUpdates?` - Function for batching React updates
842
+ - `unusedCacheTime?` - Cache time for unused stores
843
+ - `disableDevtools?` - Whether to disable devtools
844
+ - `confirmUnsavedChanges?` - beforeunload confirmation
845
+ - `otelOptions?` - OpenTelemetry configuration
846
+ - `debug?` - Debug options
847
+ - `runtime?` - Effect runtime for registry operations
886
848
 
887
- #### `StoreRegistryProvider`
849
+ ### `<StoreRegistryProvider>`
888
850
 
889
- React context provider that supplies the registry to components.
851
+ React context provider that makes a [`StoreRegistry`](#new-storeregistryconfig) available to descendant components.
890
852
 
891
853
  Props:
892
- - `storeRegistry` - The registry instance (required)
893
- - `children` - React nodes (required)
854
+ - `storeRegistry` - The registry instance
855
+
856
+ ### `useStoreRegistry(override?)`
857
+
858
+ Hook that returns the [`StoreRegistry`](#new-storeregistryconfig) provided by the nearest [`<StoreRegistryProvider>`](#storeregistryprovider) ancestor, or the `override` if provided.
859
+
860
+ ### `storeRegistry.preload(options)`
861
+
862
+ Loads a store (without suspending) to warm up the cache. Returns a Promise that resolves when loading completes. This is a fire-and-forget operation useful for warming up the cache.
863
+
864
+ ## Framework-Specific Notes
865
+
866
+ ### Vite
867
+
868
+ LiveStore works with Vite out of the box.
869
+
870
+ ### Tanstack Start
871
+
872
+ LiveStore works with Tanstack Start out of the box.
873
+
874
+ #### Provider Placement
875
+
876
+ When using LiveStore with TanStack Start, place [`<StoreRegistryProvider>`](#storeregistryprovider) in the correct location to avoid remounting on navigation.
877
+
878
+ :::caution
879
+ **Do NOT place `<StoreRegistryProvider>` inside `shellComponent`**. The `shellComponent` can be re-rendered on navigation, causing LiveStore to remount and show the loading screen on every page transition.
880
+ :::
881
+
882
+ Use the `component` prop on `createRootRoute` for `<StoreRegistryProvider>`:
883
+
884
+ ```tsx
894
885
 
895
- #### `useStore(options)`
886
+ export const Route = createRootRoute({
887
+ shellComponent: RootShell, // HTML structure only - NO state or providers
888
+ component: RootComponent, // App shell - StoreRegistryProvider goes HERE
889
+ })
896
890
 
897
- Hook that returns a store instance, suspending until it's loaded.
891
+ // HTML document shell - keep this stateless
892
+ function RootShell({ children }: { children: React.ReactNode }) {
893
+ return (
894
+ <html lang="en">
895
+ <head><HeadContent /></head>
896
+ <body>
897
+ {children}
898
+ <Scripts />
899
+ </body>
900
+ </html>
901
+ )
902
+ }
898
903
 
899
- - Throws a Promise during loading (for React Suspense)
900
- - Throws an Error if loading fails (for Error Boundaries)
901
- - Returns the loaded store when ready
904
+ // App shell - persists across SPA navigation
905
+ function RootComponent() {
906
+ const [storeRegistry] = useState(() => new StoreRegistry())
902
907
 
903
- #### `useStoreRegistry()`
908
+ return (
909
+ <Suspense fallback={<div>Loading LiveStore...</div>}>
910
+ <StoreRegistryProvider storeRegistry={storeRegistry}>
911
+ <Outlet />
912
+ </StoreRegistryProvider>
913
+ </Suspense>
914
+ )
915
+ }
916
+ ```
904
917
 
905
- Returns the current `StoreRegistry` from context. Useful for advanced operations like preloading.
918
+ TanStack Start's `shellComponent` is designed for SSR HTML streaming and may be re-evaluated on server requests during navigation. When `<StoreRegistryProvider>` is placed there, the WebSocket connection is re-established and all LiveStore state is re-initialized on each navigation.
906
919
 
907
- #### `registry.preload(options)`
920
+ If you see the loading screen on every navigation, check your server logs for multiple "Launching WebSocket" messages.
908
921
 
909
- Starts loading a store without suspending. Returns a Promise that resolves when loading completes (or rejects on error). This is a fire-and-forget operation.
922
+ ### Expo / React Native
923
+
924
+ LiveStore has a first-class integration with Expo / React Native via `@livestore/adapter-expo`. See the [Expo Adapter documentation](/platform-adapters/expo-adapter).
925
+
926
+ ### Next.js
927
+
928
+ Given various Next.js limitations, LiveStore doesn't yet work with Next.js out of the box.
910
929
 
911
930
  ### Complete Example
912
931
 
913
932
  See the <a href={`https://github.com/livestorejs/livestore/tree/${getBranchName()}/examples/web-multi-store`}>Multi-Store example</a> for a complete working application demonstrating various multi-store patterns.
914
933
 
915
- ## Technical notes
934
+ ## Technical Notes
916
935
 
917
- - `@livestore/react` uses `React.useState` under the hood for `useQuery` / `useClientDocument` to bind LiveStore's reactivity to React's reactivity. Some libraries are using `React.useExternalSyncStore` for similar purposes but using `React.useState` in this case is more efficient and all that's needed for LiveStore.
936
+ - `@livestore/react` uses `React.useState()` under the hood for `useQuery()` / `useClientDocument()` to bind LiveStore's reactivity to React's reactivity. Some libraries use `React.useSyncExternalStore()` for similar purposes but `React.useState()` is more efficient for LiveStore's architecture.
918
937
  - `@livestore/react` supports React Strict Mode.